From 93fc8aa14c914d03643197306c325becbaed2581 Mon Sep 17 00:00:00 2001 From: beatrix-bitrot Date: Mon, 5 Jun 2017 22:34:08 +0000 Subject: keyword muting and local only tooting WIP --- app/lib/feed_manager.rb | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'app/lib') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 90a1441f2..1885eff26 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -95,6 +95,12 @@ class FeedManager end def filter_from_home?(status, receiver_id) + # extremely violent filtering code BEGIN + #filter_string = 'e' + #reggie = Regexp.new(filter_string) + #return true if reggie === status.content || reggie === status.spoiler_text + # extremely violent filtering code END + return true if status.reply? && status.in_reply_to_id.nil? check_for_mutes = [status.account_id] -- cgit From 6107e954040443d1fc4344b440b229d70fe75166 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Wed, 28 Jun 2017 00:27:44 -0700 Subject: Backend YAML Processing + Profile Metadata on Static Pages --- app/javascript/styles/accounts.scss | 133 ++++++++++--------- app/lib/frontmatter_handler.rb | 242 +++++++++++++++++++++++++++++++++++ app/views/accounts/_header.html.haml | 45 ++++--- 3 files changed, 337 insertions(+), 83 deletions(-) create mode 100644 app/lib/frontmatter_handler.rb (limited to 'app/lib') diff --git a/app/javascript/styles/accounts.scss b/app/javascript/styles/accounts.scss index 10f8bd2b9..d346a6bb2 100644 --- a/app/javascript/styles/accounts.scss +++ b/app/javascript/styles/accounts.scss @@ -1,29 +1,34 @@ .card { + display: flex; background: $ui-base-color; - background-size: cover; - background-position: center; - padding: 60px 0; - padding-bottom: 0; border-radius: 4px 4px 0 0; box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); overflow: hidden; - position: relative; @media screen and (max-width: 700px) { border-radius: 0; box-shadow: none; } - &::after { - background: linear-gradient(rgba($base-shadow-color, 0.5), rgba($base-shadow-color, 0.8)); - display: block; - content: ""; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - z-index: 1; + .details { + position: relative; + padding: 60px 0 0; + background-size: cover; + background-position: center; + text-align: center; + flex: auto; + + &::after { + background: linear-gradient(rgba($base-shadow-color, 0.5), rgba($base-shadow-color, 0.8)); + display: block; + content: ""; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: 1; + } } .name { @@ -66,57 +71,36 @@ z-index: 2; } - .details { - display: flex; - margin-top: 30px; - position: relative; - z-index: 2; - flex-direction: row; - } - .details-counters { - display: flex; + position: relative; + display: inline-flex; flex-direction: row; - order: 0; + margin: 15px 0; + z-index: 2; } .counter { width: 80px; color: $ui-primary-color; padding: 5px 10px 0; - margin-bottom: 10px; - border-right: 1px solid $ui-primary-color; cursor: default; position: relative; - a { - display: block; + & + .counter { + border-left: 1px solid $ui-primary-color; } - &::after { - display: block; - content: ""; - position: absolute; - bottom: -10px; - left: 0; - width: 100%; - border-bottom: 4px solid $ui-primary-color; - opacity: 0.5; - transition: all 0.8s ease; + & > * { + opacity: .7; + transition: opacity .3s ease; } - &.active { - &::after { - border-bottom: 4px solid $ui-highlight-color; - opacity: 1; - } + &.active > *, &:hover > * { + opacity: 1; } - &:hover { - &::after { - opacity: 1; - transition-duration: 0.2s; - } + a { + display: block; } a { @@ -140,30 +124,51 @@ } .bio { - flex: 1; + position: relative; font-size: 14px; line-height: 18px; + margin: 15px 0; padding: 5px 10px; color: $ui-secondary-color; - order: 1; + z-index: 2; } - @media screen and (max-width: 480px) { - .details { - display: block; - } + .metadata { + max-width: 40%; + background: $ui-base-color; + color: $primary-text-color; + text-align: left; + overflow-y: auto; + white-space: pre-wrap; - .bio { - text-align: center; - margin-bottom: 20px; - } + .metadata-item { + border-bottom: 1px $ui-primary-color solid; + padding: 15px 10px; + font-size: 18px; + line-height: 24px; + overflow: hidden; + text-overflow: ellipsis; - .counter { - flex: 1 1 auto; - } + a { + color: $ui-highlight-color; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } - .counter:last-child { - border-right: none; + b { + display: block; + font-size: 12px; + line-height: 16px; + text-transform: uppercase; + color: $ui-primary-color; + + a { + color: $ui-primary-color; + } + } } } } diff --git a/app/lib/frontmatter_handler.rb b/app/lib/frontmatter_handler.rb new file mode 100644 index 000000000..19007cf4e --- /dev/null +++ b/app/lib/frontmatter_handler.rb @@ -0,0 +1,242 @@ +# frozen_string_literal: true + +require 'singleton' + +# See also `app/javascript/features/account/util/bio_metadata.js`. + +class FrontmatterHandler + include Singleton + + # CONVENIENCE FUNCTIONS # + + def self.unirex(str) + Regexp.new str, false, 'um' + end + def self.rexstr(exp) + '(?:' + exp.source + ')' + end + + # CHARACTER CLASSES # + + DOCUMENT_START = /^/ + DOCUMENT_END = /$/ + ALLOWED_CHAR = # c-printable` in the YAML 1.2 spec. + /[\t\n\r\u{20}-\u{7e}\u{85}\u{a0}-\u{d7ff}\u{e000}-\u{fffd}\u{10000}-\u{10ffff}]/u + WHITE_SPACE = /[ \t]/ + INDENTATION = / */ + LINE_BREAK = /\r?\n|\r|/ + ESCAPE_CHAR = /[0abt\tnvfre "\/\\N_LP]/ + HEXADECIMAL_CHARS = /[0-9a-fA-F]/ + INDICATOR = /[-?:,\[\]{}&#*!|>'"%@`]/ + FLOW_CHAR = /[,\[\]{}]/ + + # NEGATED CHARACTER CLASSES # + + NOT_WHITE_SPACE = unirex '(?!' + rexstr(WHITE_SPACE) + ').' + NOT_LINE_BREAK = unirex '(?!' + rexstr(LINE_BREAK) + ').' + NOT_INDICATOR = unirex '(?!' + rexstr(INDICATOR) + ').' + NOT_FLOW_CHAR = unirex '(?!' + rexstr(FLOW_CHAR) + ').' + + # BASIC CONSTRUCTS # + + ANY_WHITE_SPACE = unirex rexstr(WHITE_SPACE) + '*' + ANY_ALLOWED_CHARS = unirex rexstr(ALLOWED_CHAR) + '*' + NEW_LINE = unirex( + rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) + ) + SOME_NEW_LINES = unirex( + '(?:' + rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) + ')+' + ) + POSSIBLE_STARTS = unirex( + rexstr(DOCUMENT_START) + rexstr(/]*>/) + '?' + ) + POSSIBLE_ENDS = unirex( + rexstr(SOME_NEW_LINES) + '|' + + rexstr(DOCUMENT_END) + '|' + + rexstr(/<\/p>/) + ) + CHARACTER_ESCAPE = unirex( + rexstr(/\\/) + + '(?:' + + rexstr(ESCAPE_CHAR) + '|' + + rexstr(/x/) + rexstr(HEXADECIMAL_CHARS) + '{2}' + '|' + + rexstr(/u/) + rexstr(HEXADECIMAL_CHARS) + '{4}' + '|' + + rexstr(/U/) + rexstr(HEXADECIMAL_CHARS) + '{8}' + + ')' + ) + ESCAPED_CHAR = unirex( + rexstr(/(?!["\\])/) + rexstr(NOT_LINE_BREAK) + '|' + + rexstr(CHARACTER_ESCAPE) + ) + ANY_ESCAPED_CHARS = unirex( + rexstr(ESCAPED_CHAR) + '*' + ) + ESCAPED_APOS = unirex( + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + rexstr(/[^']|''/) + ) + ANY_ESCAPED_APOS = unirex( + rexstr(ESCAPED_APOS) + '*' + ) + FIRST_KEY_CHAR = unirex( + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + + rexstr(NOT_INDICATOR) + '|' + + rexstr(/[?:-]/) + + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + + '(?=' + rexstr(NOT_FLOW_CHAR) + ')' + ) + FIRST_VALUE_CHAR = unirex( + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + + rexstr(NOT_INDICATOR) + '|' + + rexstr(/[?:-]/) + + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + # Flow indicators are allowed in values. + ) + LATER_KEY_CHAR = unirex( + rexstr(WHITE_SPACE) + '|' + + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + + '(?=' + rexstr(NOT_FLOW_CHAR) + ')' + + rexstr(/[^:#]#?/) + '|' + + rexstr(/:/) + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + ) + LATER_VALUE_CHAR = unirex( + rexstr(WHITE_SPACE) + '|' + + '(?=' + rexstr(NOT_LINE_BREAK) + ')' + + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + + # Flow indicators are allowed in values. + rexstr(/[^:#]#?/) + '|' + + rexstr(/:/) + '(?=' + rexstr(NOT_WHITE_SPACE) + ')' + ) + + # YAML CONSTRUCTS # + + YAML_START = unirex( + rexstr(ANY_WHITE_SPACE) + rexstr(/---/) + ) + YAML_END = unirex( + rexstr(ANY_WHITE_SPACE) + rexstr(/(?:---|\.\.\.)/) + ) + YAML_LOOKAHEAD = unirex( + '(?=' + + rexstr(YAML_START) + + rexstr(ANY_ALLOWED_CHARS) + rexstr(NEW_LINE) + + rexstr(YAML_END) + rexstr(POSSIBLE_ENDS) + + ')' + ) + YAML_DOUBLE_QUOTE = unirex( + rexstr(/"/) + rexstr(ANY_ESCAPED_CHARS) + rexstr(/"/) + ) + YAML_SINGLE_QUOTE = unirex( + rexstr(/'/) + rexstr(ANY_ESCAPED_APOS) + rexstr(/'/) + ) + YAML_SIMPLE_KEY = unirex( + rexstr(FIRST_KEY_CHAR) + rexstr(LATER_KEY_CHAR) + '*' + ) + YAML_SIMPLE_VALUE = unirex( + rexstr(FIRST_VALUE_CHAR) + rexstr(LATER_VALUE_CHAR) + '*' + ) + YAML_KEY = unirex( + rexstr(YAML_DOUBLE_QUOTE) + '|' + + rexstr(YAML_SINGLE_QUOTE) + '|' + + rexstr(YAML_SIMPLE_KEY) + ) + YAML_VALUE = unirex( + rexstr(YAML_DOUBLE_QUOTE) + '|' + + rexstr(YAML_SINGLE_QUOTE) + '|' + + rexstr(YAML_SIMPLE_VALUE) + ) + YAML_SEPARATOR = unirex( + rexstr(ANY_WHITE_SPACE) + + ':' + rexstr(WHITE_SPACE) + + rexstr(ANY_WHITE_SPACE) + ) + YAML_LINE = unirex( + '(' + rexstr(YAML_KEY) + ')' + + rexstr(YAML_SEPARATOR) + + '(' + rexstr(YAML_VALUE) + ')' + ) + + # FRONTMATTER REGEX # + + YAML_FRONTMATTER = unirex( + rexstr(POSSIBLE_STARTS) + + rexstr(YAML_LOOKAHEAD) + + rexstr(YAML_START) + rexstr(SOME_NEW_LINES) + + '(?:' + + '(' + rexstr(INDENTATION) + ')' + + rexstr(YAML_LINE) + rexstr(SOME_NEW_LINES) + + '(?:' + + '\\1' + rexstr(YAML_LINE) + rexstr(SOME_NEW_LINES) + + '){0,4}' + + ')?' + + rexstr(YAML_END) + rexstr(POSSIBLE_ENDS) + ) + + # SEARCHES # + + FIND_YAML_LINES = unirex( + rexstr(NEW_LINE) + rexstr(INDENTATION) + rexstr(YAML_LINE) + ) + + # STRING PROCESSING # + + def process_string(str) + case str[0] + when '"' + str[1..-2] + .gsub(/\\0/, "\u{00}") + .gsub(/\\a/, "\u{07}") + .gsub(/\\b/, "\u{08}") + .gsub(/\\t/, "\u{09}") + .gsub(/\\n/, "\u{0a}") + .gsub(/\\v/, "\u{0b}") + .gsub(/\\f/, "\u{0c}") + .gsub(/\\r/, "\u{0d}") + .gsub(/\\e/, "\u{1b}") + .gsub(/\\ /, "\u{20}") + .gsub(/\\"/, "\u{22}") + .gsub(/\\\//, "\u{2f}") + .gsub(/\\\\/, "\u{5c}") + .gsub(/\\N/, "\u{85}") + .gsub(/\\_/, "\u{a0}") + .gsub(/\\L/, "\u{2028}") + .gsub(/\\P/, "\u{2029}") + .gsub(/\\x([0-9a-fA-F]{2})/mu) {|s| $1.to_i.chr Encoding::UTF_8} + .gsub(/\\u([0-9a-fA-F]{4})/mu) {|s| $1.to_i.chr Encoding::UTF_8} + .gsub(/\\U([0-9a-fA-F]{8})/mu) {|s| $1.to_i.chr Encoding::UTF_8} + when "'" + str[1..-2].gsub(/''/, "'") + else + str + end + end + + # BIO PROCESSING # + + def process_bio content + result = { + text: content.gsub(/"/, '"').gsub(/'/, "'"), + metadata: [] + } + yaml = YAML_FRONTMATTER.match(result[:text]) + return result unless yaml + yaml = yaml[0] + start = YAML_START =~ result[:text] + ending = start + yaml.length - (YAML_START =~ yaml) + result[:text][start..ending - 1] = '' + metadata = nil + index = 0 + while metadata = FIND_YAML_LINES.match(yaml, index) do + index = metadata.end(0) + result[:metadata].push [ + process_string(metadata[1]), process_string(metadata[2]) + ] + end + return result + end + +end diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index 6451a5573..ef6a2bcbe 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -1,23 +1,24 @@ -.card.h-card.p-author{ style: "background-image: url(#{account.header.url(:original)})" } - - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account) - .controls - - if current_account.following?(account) - = link_to t('accounts.unfollow'), account_unfollow_path(account), data: { method: :post }, class: 'button' - - else - = link_to t('accounts.follow'), account_follow_path(account), data: { method: :post }, class: 'button' - - elsif !user_signed_in? - .controls - .remote-follow - = link_to t('accounts.remote_follow'), account_remote_follow_path(account), class: 'button' - .avatar= image_tag account.avatar.url(:original), class: 'u-photo' - %h1.name - %span.p-name.emojify= display_name(account) - %small - %span @#{account.username} - = fa_icon('lock') if account.locked? - .details +- processed_bio = FrontmatterHandler.instance.process_bio Formatter.instance.simplified_format account +.card.h-card.p-author + .details{ style: "background-image: url(#{account.header.url(:original)})" } + - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account) + .controls + - if current_account.following?(account) + = link_to t('accounts.unfollow'), account_unfollow_path(account), data: { method: :post }, class: 'button' + - else + = link_to t('accounts.follow'), account_follow_path(account), data: { method: :post }, class: 'button' + - elsif !user_signed_in? + .controls + .remote-follow + = link_to t('accounts.remote_follow'), account_remote_follow_path(account), class: 'button' + .avatar= image_tag account.avatar.url(:original), class: 'u-photo' + %h1.name + %span.p-name.emojify= display_name(account) + %small + %span @#{account.username} + = fa_icon('lock') if account.locked? .bio - .account__header__content.p-note.emojify= Formatter.instance.simplified_format(account) + .account__header__content.p-note.emojify!=processed_bio[:text] .details-counters .counter{ class: active_nav_class(short_account_url(account)) } @@ -32,3 +33,9 @@ = link_to account_followers_url(account) do %span.counter-label= t('accounts.followers') %span.counter-number= number_with_delimiter account.followers_count + - if processed_bio[:metadata].length > 0 + .metadata< + - processed_bio[:metadata].each do |i| + .metadata-item>< + %b.emojify>!=i[0] + %span.emojify>!=i[1] -- cgit From 6cbbdc805f68b0318c9b3a7c01f4bdaefee5a2fa Mon Sep 17 00:00:00 2001 From: kibigo! Date: Wed, 28 Jun 2017 15:16:13 -0700 Subject: Fixed Regexp.new syntax --- app/lib/frontmatter_handler.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/lib') diff --git a/app/lib/frontmatter_handler.rb b/app/lib/frontmatter_handler.rb index 19007cf4e..4ee43f092 100644 --- a/app/lib/frontmatter_handler.rb +++ b/app/lib/frontmatter_handler.rb @@ -10,7 +10,7 @@ class FrontmatterHandler # CONVENIENCE FUNCTIONS # def self.unirex(str) - Regexp.new str, false, 'um' + Regexp.new str, Regexp::MULTILINE, 'u' end def self.rexstr(exp) '(?:' + exp.source + ')' -- cgit From c58877862d8622b009e473205a6322e013849a7e Mon Sep 17 00:00:00 2001 From: kibigo! Date: Fri, 30 Jun 2017 15:03:31 -0700 Subject: createBio function added --- .../mastodon/features/account/util/bio_metadata.js | 85 ++++++++++++++++++++++ app/lib/frontmatter_handler.rb | 2 + 2 files changed, 87 insertions(+) (limited to 'app/lib') diff --git a/app/javascript/mastodon/features/account/util/bio_metadata.js b/app/javascript/mastodon/features/account/util/bio_metadata.js index fc24549b6..bdbb1750b 100644 --- a/app/javascript/mastodon/features/account/util/bio_metadata.js +++ b/app/javascript/mastodon/features/account/util/bio_metadata.js @@ -69,6 +69,9 @@ const NOT_WHITE_SPACE = unirex('(?!' + rexstr(WHITE_SPACE) + ')[^]'); const NOT_LINE_BREAK = unirex('(?!' + rexstr(LINE_BREAK) + ')[^]'); const NOT_INDICATOR = unirex('(?!' + rexstr(INDICATOR) + ')[^]'); const NOT_FLOW_CHAR = unirex('(?!' + rexstr(FLOW_CHAR) + ')[^]'); +const NOT_ALLOWED_CHAR = unirex( + '(?!' + rexstr(ALLOWED_CHAR) + ')[^]' +); /* BASIC CONSTRUCTS */ @@ -226,6 +229,7 @@ function processString(str) { .replace(/\\a/g, '\x07') .replace(/\\b/g, '\x08') .replace(/\\t/g, '\x09') + .replace(/\\\x09/g, '\x09') .replace(/\\n/g, '\x0a') .replace(/\\v/g, '\x0b') .replace(/\\f/g, '\x0c') @@ -293,3 +297,84 @@ export function processBio(content) { } return result; } + +/* BIO CREATION */ + +export function createBio(note, data) { + if (!note) note = ''; + let frontmatter = ''; + if ((data && data.length) || note.match(/^\s*---\s+/)) { + if (!data) frontmatter = '---\n...\n'; + else { + frontmatter += '---\n'; + for (let i = 0; i < data.length; i++) { + let key = '' + data[i][0]; + let val = '' + data[i][1]; + + // Key processing + if (key === (key.match(YAML_SIMPLE_KEY) || [])[0]) /* do nothing */; + else if (key.indexOf('\'') === -1 && key === (key.match(ANY_ESCAPED_APOS) || [])[0]) key = '\'' + key + '\''; + else { + key = key + .replace(/\x00/g, '\\0') + .replace(/\x07/g, '\\a') + .replace(/\x08/g, '\\b') + .replace(/\x0a/g, '\\n') + .replace(/\x0b/g, '\\v') + .replace(/\x0c/g, '\\f') + .replace(/\x0d/g, '\\r') + .replace(/\x1b/g, '\\e') + .replace(/\x22/g, '\\"') + .replace(/\x5c/g, '\\\\'); + let badchars = key.match( + new RegExp(rexstr(NOT_ALLOWED_CHAR), 'gu') + ) || []; + for (let j = 0; j < badchars.length; j++) { + key = key.replace( + badchars[i], + '\\u' + badchars[i].codePointAt(0).toLocaleString('en', { + useGrouping: false, + minimumIntegerDigits: 4, + }) + ); + } + key = '"' + key + '"'; + } + + // Value processing + if (val === (val.match(YAML_SIMPLE_VALUE) || [])[0]) /* do nothing */; + else if (val.indexOf('\'') === -1 && val === (val.match(ANY_ESCAPED_APOS) || [])[0]) val = '\'' + val + '\''; + else { + val = val + .replace(/\x00/g, '\\0') + .replace(/\x07/g, '\\a') + .replace(/\x08/g, '\\b') + .replace(/\x0a/g, '\\n') + .replace(/\x0b/g, '\\v') + .replace(/\x0c/g, '\\f') + .replace(/\x0d/g, '\\r') + .replace(/\x1b/g, '\\e') + .replace(/\x22/g, '\\"') + .replace(/\x5c/g, '\\\\'); + let badchars = val.match( + new RegExp(rexstr(NOT_ALLOWED_CHAR), 'gu') + ) || []; + for (let j = 0; j < badchars.length; j++) { + val = val.replace( + badchars[i], + '\\u' + badchars[i].codePointAt(0).toLocaleString('en', { + useGrouping: false, + minimumIntegerDigits: 4, + }) + ); + } + val = '"' + val + '"'; + } + + frontmatter += key + ': ' + val + '\n'; + } + frontmatter += '...\n'; + } + } + return frontmatter + note; +} diff --git a/app/lib/frontmatter_handler.rb b/app/lib/frontmatter_handler.rb index 4ee43f092..83e5f465e 100644 --- a/app/lib/frontmatter_handler.rb +++ b/app/lib/frontmatter_handler.rb @@ -36,6 +36,7 @@ class FrontmatterHandler NOT_LINE_BREAK = unirex '(?!' + rexstr(LINE_BREAK) + ').' NOT_INDICATOR = unirex '(?!' + rexstr(INDICATOR) + ').' NOT_FLOW_CHAR = unirex '(?!' + rexstr(FLOW_CHAR) + ').' + NOT_ALLOWED_CHAR = unirex '(?!' + rexstr(ALLOWED_CHAR) + ').' # BASIC CONSTRUCTS # @@ -192,6 +193,7 @@ class FrontmatterHandler .gsub(/\\a/, "\u{07}") .gsub(/\\b/, "\u{08}") .gsub(/\\t/, "\u{09}") + .gsub(/\\\u{09}/, "\u{09}") .gsub(/\\n/, "\u{0a}") .gsub(/\\v/, "\u{0b}") .gsub(/\\f/, "\u{0c}") -- cgit From 86e617a839bd4eb45ace52ab226a4e93bb184ff0 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Mon, 25 Sep 2017 19:24:32 -0700 Subject: Better themeing support!! --- app/controllers/application_controller.rb | 6 ++++ app/controllers/settings/preferences_controller.rb | 1 + .../local_settings/navigation/item/style.scss | 2 +- .../local_settings/navigation/style.scss | 2 +- .../components/local_settings/page/item/style.scss | 2 +- .../components/local_settings/page/style.scss | 2 +- .../glitch/components/local_settings/style.scss | 2 +- app/javascript/packs/application.js | 2 +- app/javascript/packs/frontends/mastodon.js | 16 ---------- app/javascript/styles/custom.scss | 1 - app/javascript/themes/default/theme.yml | 9 ++++++ app/javascript/themes/spin/pack.js | 2 ++ app/javascript/themes/spin/style.scss | 14 ++++++++ app/javascript/themes/spin/theme.yml | 2 ++ app/lib/themes.rb | 23 ++++++++++++++ app/lib/user_settings_decorator.rb | 5 +++ app/models/user.rb | 4 +++ app/views/home/index.html.haml | 4 +-- app/views/settings/preferences/show.html.haml | 2 ++ config/initializers/frontends.rb | 7 ---- config/locales/simple_form.en.yml | 2 ++ config/settings.yml | 1 + config/webpack/configuration.js | 17 +++++++++- config/webpack/loaders/sass.js | 2 +- config/webpack/shared.js | 37 ++++++++++++---------- 25 files changed, 117 insertions(+), 50 deletions(-) delete mode 100644 app/javascript/packs/frontends/mastodon.js delete mode 100644 app/javascript/styles/custom.scss create mode 100644 app/javascript/themes/default/theme.yml create mode 100644 app/javascript/themes/spin/pack.js create mode 100644 app/javascript/themes/spin/style.scss create mode 100644 app/javascript/themes/spin/theme.yml create mode 100644 app/lib/themes.rb delete mode 100644 config/initializers/frontends.rb (limited to 'app/lib') diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0b40fb05b..d5eca6ffb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base helper_method :current_account helper_method :current_session + helper_method :current_theme helper_method :single_user_mode? rescue_from ActionController::RoutingError, with: :not_found @@ -77,6 +78,11 @@ class ApplicationController < ActionController::Base @current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id']) end + def current_theme + return Setting.default_settings['theme'] unless Themes.instance.names.include? current_user&.setting_theme + current_user.setting_theme + end + def cache_collection(raw, klass) return raw unless klass.respond_to?(:with_includes) diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index f107f2b16..207c7b324 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -41,6 +41,7 @@ class Settings::PreferencesController < ApplicationController :setting_auto_play_gif, :setting_system_font_ui, :setting_noindex, + :setting_theme, notification_emails: %i(follow follow_request reblog favourite mention digest), interactions: %i(must_be_follower must_be_following) ) diff --git a/app/javascript/glitch/components/local_settings/navigation/item/style.scss b/app/javascript/glitch/components/local_settings/navigation/item/style.scss index 505c86912..33d7d3744 100644 --- a/app/javascript/glitch/components/local_settings/navigation/item/style.scss +++ b/app/javascript/glitch/components/local_settings/navigation/item/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings__navigation__item { display: block; diff --git a/app/javascript/glitch/components/local_settings/navigation/style.scss b/app/javascript/glitch/components/local_settings/navigation/style.scss index 1cc39e3e9..a610a1212 100644 --- a/app/javascript/glitch/components/local_settings/navigation/style.scss +++ b/app/javascript/glitch/components/local_settings/navigation/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings__navigation { background: $primary-text-color; diff --git a/app/javascript/glitch/components/local_settings/page/item/style.scss b/app/javascript/glitch/components/local_settings/page/item/style.scss index e614030c0..da1941b99 100644 --- a/app/javascript/glitch/components/local_settings/page/item/style.scss +++ b/app/javascript/glitch/components/local_settings/page/item/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings__page__item { select { diff --git a/app/javascript/glitch/components/local_settings/page/style.scss b/app/javascript/glitch/components/local_settings/page/style.scss index 7269056c3..53c95ea40 100644 --- a/app/javascript/glitch/components/local_settings/page/style.scss +++ b/app/javascript/glitch/components/local_settings/page/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings__page { display: block; diff --git a/app/javascript/glitch/components/local_settings/style.scss b/app/javascript/glitch/components/local_settings/style.scss index 6f7fcbaa4..54fec47bd 100644 --- a/app/javascript/glitch/components/local_settings/style.scss +++ b/app/javascript/glitch/components/local_settings/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings { position: relative; diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index c06714dc1..aa94006c6 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -2,7 +2,7 @@ import loadPolyfills from '../mastodon/load_polyfills'; // import default stylesheet with variables require('font-awesome/css/font-awesome.css'); -require('mastodon-application-style'); +import 'styles/application'; require.context('../images/', true); diff --git a/app/javascript/packs/frontends/mastodon.js b/app/javascript/packs/frontends/mastodon.js deleted file mode 100644 index a983de36f..000000000 --- a/app/javascript/packs/frontends/mastodon.js +++ /dev/null @@ -1,16 +0,0 @@ -// This file replaces `app/javascript/packs/application.js` for use -// with multiple frontends. - -import loadPolyfills from '../../mastodon/load_polyfills'; - -// import default stylesheet with variables -require('font-awesome/css/font-awesome.css'); -require('mastodon-application-style'); - -require.context('../../images/', true); - -loadPolyfills().then(() => { - require('../../mastodon/main').default(); -}).catch(e => { - console.error(e); -}); diff --git a/app/javascript/styles/custom.scss b/app/javascript/styles/custom.scss deleted file mode 100644 index 97a981243..000000000 --- a/app/javascript/styles/custom.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'application'; diff --git a/app/javascript/themes/default/theme.yml b/app/javascript/themes/default/theme.yml new file mode 100644 index 000000000..6a7a872b4 --- /dev/null +++ b/app/javascript/themes/default/theme.yml @@ -0,0 +1,9 @@ +# (REQUIRED) Name must be unique across all installed themes. +name: default + +# (REQUIRED) The location of the pack file inside `pack_directory`. +pack: application.js + +# (OPTIONAL) The directory which contains the pack file. +# Defaults to the theme directory (`app/javascript/themes/[theme]`). +pack_directory: app/javascript/packs diff --git a/app/javascript/themes/spin/pack.js b/app/javascript/themes/spin/pack.js new file mode 100644 index 000000000..dab0e93a4 --- /dev/null +++ b/app/javascript/themes/spin/pack.js @@ -0,0 +1,2 @@ +import 'packs/application'; +import 'themes/spin/style'; diff --git a/app/javascript/themes/spin/style.scss b/app/javascript/themes/spin/style.scss new file mode 100644 index 000000000..1a9381fd0 --- /dev/null +++ b/app/javascript/themes/spin/style.scss @@ -0,0 +1,14 @@ +:root:root:root { + .button, .icon-button, .emoji-button, .account__avatar, .account__avatar-overlay { + animation: spin 4s linear infinite; + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/app/javascript/themes/spin/theme.yml b/app/javascript/themes/spin/theme.yml new file mode 100644 index 000000000..a684997dc --- /dev/null +++ b/app/javascript/themes/spin/theme.yml @@ -0,0 +1,2 @@ +name: spin +pack: pack.js \ No newline at end of file diff --git a/app/lib/themes.rb b/app/lib/themes.rb new file mode 100644 index 000000000..2dd188297 --- /dev/null +++ b/app/lib/themes.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'singleton' +require 'yaml' + +class Themes + include Singleton + + def initialize + result = Hash.new + Dir.glob(Rails.root.join('app', 'javascript', 'themes', '*', 'theme.yml')) do |path| + data = YAML.load_file(path) + if data['pack'] && data['name'] + result[data['name']] = data + end + end + @conf = result + end + + def names + @conf.keys + end +end diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 62046ed72..3b156b98c 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -25,6 +25,7 @@ class UserSettingsDecorator user.settings['auto_play_gif'] = auto_play_gif_preference user.settings['system_font_ui'] = system_font_ui_preference user.settings['noindex'] = noindex_preference + user.settings['theme'] = theme_preference end def merged_notification_emails @@ -67,6 +68,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_noindex' end + def theme_preference + settings['setting_theme'] + end + def boolean_cast_setting(key) settings[key] == '1' end diff --git a/app/models/user.rb b/app/models/user.rb index 5e548c1ef..3bf069a31 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -110,6 +110,10 @@ class User < ApplicationRecord settings.noindex end + def setting_theme + settings.theme + end + def token_for_app(a) return nil if a.nil? || a.owner != self Doorkeeper::AccessToken diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index a13d0702b..3b4219c56 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -2,8 +2,8 @@ %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key} %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) - = javascript_pack_tag "frontends/#{@frontend}", integrity: true, crossorigin: 'anonymous' - = stylesheet_pack_tag "frontends/#{@frontend}", integrity: true, media: 'all' + = javascript_pack_tag "themes/#{current_theme}", integrity: true, crossorigin: 'anonymous' + = stylesheet_pack_tag "themes/#{current_theme}", integrity: true, media: 'all' .app-holder#mastodon{ data: { props: Oj.dump(default_props) } } %noscript diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index f42f92508..5efd538e4 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -5,6 +5,8 @@ = render 'shared/error_messages', object: current_user .fields-group + = f.input :setting_theme, collection: Themes.instance.names, label_method: lambda { |theme| safe_join([I18n.t("themes.#{theme}", default: theme)])}, wrapper: :with_label, include_blank: false + = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, diff --git a/config/initializers/frontends.rb b/config/initializers/frontends.rb deleted file mode 100644 index 2cb68cc61..000000000 --- a/config/initializers/frontends.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Be sure to restart your server when you modify this file. - -Rails.application.configure do - frontends = [] - Rails.root.join('app', 'javascript', 'packs', 'frontends').each_child(false) { |f| frontends.push f.to_s } - config.x.available_frontends = frontends -end diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index fb8524a24..f9d4e2e52 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -13,6 +13,7 @@ en: one: 1 character left other: %{count} characters left setting_noindex: Affects your public profile and status pages + setting_theme: Affects how Mastodon looks when you're logged in from any device. imports: data: CSV file exported from another Mastodon instance sessions: @@ -44,6 +45,7 @@ en: setting_noindex: Opt-out of search engine indexing setting_system_font_ui: Use system's default font setting_unfollow_modal: Show confirmation dialog before unfollowing someone + setting_theme: Site theme severity: Severity type: Import type username: Username diff --git a/config/settings.yml b/config/settings.yml index 39dfb8f55..3cd3307f4 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -24,6 +24,7 @@ defaults: &defaults auto_play_gif: false system_font_ui: false noindex: false + theme: 'default' notification_emails: follow: false reblog: false diff --git a/config/webpack/configuration.js b/config/webpack/configuration.js index 6ef484c3a..606eb97f1 100644 --- a/config/webpack/configuration.js +++ b/config/webpack/configuration.js @@ -1,13 +1,27 @@ // Common configuration for webpacker loaded from config/webpacker.yml -const { join, resolve } = require('path'); +const { dirname, join, resolve } = require('path'); const { env } = require('process'); const { safeLoad } = require('js-yaml'); const { readFileSync } = require('fs'); +const glob = require('glob'); const configPath = resolve('config', 'webpacker.yml'); const loadersDir = join(__dirname, 'loaders'); const settings = safeLoad(readFileSync(configPath), 'utf8')[env.NODE_ENV]; +const themeFiles = glob.sync('app/javascript/themes/*/theme.yml'); +const themes = {}; + +for (let i = 0; i < themeFiles.length; i++) { + const themeFile = themeFiles[i]; + const data = safeLoad(readFileSync(themeFile), 'utf8'); + if (!data.pack_directory) { + data.pack_directory = dirname(themeFile); + } + if (data.name && data.pack) { + themes[data.name] = data; + } +} function removeOuterSlashes(string) { return string.replace(/^\/*/, '').replace(/\/*$/, ''); @@ -29,6 +43,7 @@ const output = { module.exports = { settings, + themes, env, loadersDir, output, diff --git a/config/webpack/loaders/sass.js b/config/webpack/loaders/sass.js index 40e81b43b..96ad7abe8 100644 --- a/config/webpack/loaders/sass.js +++ b/config/webpack/loaders/sass.js @@ -9,7 +9,7 @@ module.exports = { { loader: 'css-loader', options: { minimize: env.NODE_ENV === 'production' } }, { loader: 'postcss-loader', options: { sourceMap: true } }, 'resolve-url-loader', - { loader: 'sass-loader', options: { includePaths: ['app/javascript/styles'] } }, + { loader: 'sass-loader', options: { includePaths: ['app/javascript'] } }, ], }), }; diff --git a/config/webpack/shared.js b/config/webpack/shared.js index be1b49421..ab925b020 100644 --- a/config/webpack/shared.js +++ b/config/webpack/shared.js @@ -1,13 +1,12 @@ // Note: You must restart bin/webpack-dev-server for changes to take effect -const { existsSync } = require('fs'); const webpack = require('webpack'); const { basename, dirname, join, relative, resolve } = require('path'); const { sync } = require('glob'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const ManifestPlugin = require('webpack-manifest-plugin'); const extname = require('path-complete-extname'); -const { env, settings, output, loadersDir } = require('./configuration.js'); +const { env, settings, themes, output, loadersDir } = require('./configuration.js'); const localePackPaths = require('./generateLocalePacks'); const extensionGlob = `**/*{${settings.extensions.join(',')}}*`; @@ -18,17 +17,27 @@ const entryPacks = [...packPaths, ...localePackPaths].filter(path => path !== jo const customApplicationStyle = resolve(join(settings.source_path, 'styles/custom.scss')); const originalApplicationStyle = resolve(join(settings.source_path, 'styles/application.scss')); +const themePaths = Object.keys(themes).reduce( + (themePaths, name) => { + themeData = themes[name]; + themePaths[`themes/${name}`] = resolve(themeData.pack_directory, themeData.pack); + return themePaths; + }, {} +); + module.exports = { - entry: entryPacks.reduce( - (map, entry) => { - const localMap = map; - let namespace = relative(join(entryPath), dirname(entry)); - if (namespace === join('..', '..', '..', 'tmp', 'packs')) { - namespace = ''; // generated by generateLocalePacks.js - } - localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry); - return localMap; - }, {} + entry: Object.assign( + entryPacks.reduce( + (map, entry) => { + const localMap = map; + let namespace = relative(join(entryPath), dirname(entry)); + if (namespace === join('..', '..', '..', 'tmp', 'packs')) { + namespace = ''; // generated by generateLocalePacks.js + } + localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry); + return localMap; + }, {} + ), themePaths ), output: { @@ -59,10 +68,6 @@ module.exports = { ], resolve: { - alias: { - 'mastodon-application-style': existsSync(customApplicationStyle) ? - customApplicationStyle : originalApplicationStyle, - }, extensions: settings.extensions, modules: [ resolve(settings.source_path), -- cgit From a7be86e875cb0eb38ca2140306f84267d819905c Mon Sep 17 00:00:00 2001 From: beatrix Date: Fri, 20 Oct 2017 10:49:54 -0400 Subject: hide mentions of muted accounts (in home col) (#190) * hide mentions of muted accounts (in home col) also cleans up some old crap * add test --- app/lib/feed_manager.rb | 7 +------ spec/lib/feed_manager_spec.rb | 7 +++++++ 2 files changed, 8 insertions(+), 6 deletions(-) (limited to 'app/lib') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 39d3333da..ca15745cb 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -138,16 +138,11 @@ class FeedManager end def filter_from_home?(status, receiver_id) - # extremely violent filtering code BEGIN - #filter_string = 'e' - #reggie = Regexp.new(filter_string) - #return true if reggie === status.content || reggie === status.spoiler_text - # extremely violent filtering code END - 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?) check_for_mutes = [status.account_id] + check_for_mutes.concat(status.mentions.pluck(:account_id)) check_for_mutes.concat([status.reblog.account_id]) if status.reblog? return true if Mute.where(account_id: receiver_id, target_account_id: check_for_mutes).any? diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 0f97a579e..1861cc6ed 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -105,6 +105,13 @@ RSpec.describe FeedManager do expect(FeedManager.instance.filter?(:home, status, bob.id)).to be true end + it 'returns true for status by followee mentioning muted account' do + bob.mute!(jeff) + bob.follow!(alice) + status = PostStatusService.new.call(alice, 'Hey @jeff') + expect(FeedManager.instance.filter?(:home, status, bob.id)).to be true + end + it 'returns true for reblog of a personally blocked domain' do alice.block_domain!('example.com') alice.follow!(jeff) -- cgit From 4745d6eeca3a422f41775ee5f31989fc036da7d6 Mon Sep 17 00:00:00 2001 From: David Yip Date: Sat, 14 Oct 2017 02:28:20 -0500 Subject: Spec out KeywordMute interface. #164. --- app/lib/feed_manager.rb | 2 ++ app/models/keyword_mute.rb | 2 ++ spec/models/keyword_mute_spec.rb | 18 +++++++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) (limited to 'app/lib') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index ca15745cb..baaa09e86 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -138,6 +138,8 @@ class FeedManager end def filter_from_home?(status, receiver_id) + return true if KeywordMute.where(account_id: receiver_id).matches?(status.text) + 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?) diff --git a/app/models/keyword_mute.rb b/app/models/keyword_mute.rb index 91816eed9..d397a1f41 100644 --- a/app/models/keyword_mute.rb +++ b/app/models/keyword_mute.rb @@ -10,4 +10,6 @@ # class KeywordMute < ApplicationRecord + def self.matches?(text) + end end diff --git a/spec/models/keyword_mute_spec.rb b/spec/models/keyword_mute_spec.rb index cd0881565..cb6e554e4 100644 --- a/spec/models/keyword_mute_spec.rb +++ b/spec/models/keyword_mute_spec.rb @@ -1,5 +1,21 @@ require 'rails_helper' RSpec.describe KeywordMute, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + describe '.matches?' do + let(:alice) { Fabricate(:account, username: 'alice').tap(&:save!) } + let(:status) { Fabricate(:status, account: alice).tap(&:save!) } + let(:keyword_mute) { Fabricate(:keyword_mute, account: alice, keyword: 'take').tap(&:save!) } + + it 'returns true if any keyword in the set matches the status text' do + status.update_attribute(:text, 'This is a hot take') + + expect(KeywordMute.where(account: alice).matches?(status.text)).to be_truthy + end + + it 'returns false if no keyword in the set matches the status text' + + describe 'matching' do + it 'is case-insensitive' + end + end end -- cgit From 603cf02b703a2df2ae6690077a3e21a5ce64b548 Mon Sep 17 00:00:00 2001 From: David Yip Date: Sat, 14 Oct 2017 20:36:53 -0500 Subject: Rework KeywordMute interface to use a matcher object; spec out matcher. #164. A matcher object that builds a match from KeywordMute data and runs it over text is, in my view, one of the easier ways to write examples for this sort of thing. --- app/lib/feed_manager.rb | 2 +- app/models/keyword_mute.rb | 31 +++++++++++++++++- spec/models/keyword_mute_spec.rb | 70 ++++++++++++++++++++++++++++++++++------ 3 files changed, 91 insertions(+), 12 deletions(-) (limited to 'app/lib') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index baaa09e86..516bd81af 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -138,7 +138,7 @@ class FeedManager end def filter_from_home?(status, receiver_id) - return true if KeywordMute.where(account_id: receiver_id).matches?(status.text) + return true if KeywordMute.matcher_for(receiver_id) =~ status.text 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?) diff --git a/app/models/keyword_mute.rb b/app/models/keyword_mute.rb index d397a1f41..d80fcaa60 100644 --- a/app/models/keyword_mute.rb +++ b/app/models/keyword_mute.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # == Schema Information # # Table name: keyword_mutes @@ -10,6 +11,34 @@ # class KeywordMute < ApplicationRecord - def self.matches?(text) + belongs_to :account, required: true + + validates_presence_of :keyword + + def self.matcher_for(account) + Rails.cache.fetch("keyword_mutes:matcher:#{account}") { Matcher.new(account) } + end + + class Matcher + attr_reader :regex + + def initialize(account) + re = String.new.tap do |str| + scoped = KeywordMute.where(account: account) + keywords = scoped.select(:id, :keyword) + count = scoped.count + + keywords.find_each.with_index do |kw, index| + str << Regexp.escape(kw.keyword.strip) + str << '|' if index < count - 1 + end + end + + @regex = /\b(?:#{re})\b/i unless re.empty? + end + + def =~(str) + @regex ? @regex =~ str : false + end end end diff --git a/spec/models/keyword_mute_spec.rb b/spec/models/keyword_mute_spec.rb index cb6e554e4..211a9b4c6 100644 --- a/spec/models/keyword_mute_spec.rb +++ b/spec/models/keyword_mute_spec.rb @@ -1,21 +1,71 @@ require 'rails_helper' RSpec.describe KeywordMute, type: :model do - describe '.matches?' do - let(:alice) { Fabricate(:account, username: 'alice').tap(&:save!) } - let(:status) { Fabricate(:status, account: alice).tap(&:save!) } - let(:keyword_mute) { Fabricate(:keyword_mute, account: alice, keyword: 'take').tap(&:save!) } + let(:alice) { Fabricate(:account, username: 'alice').tap(&:save!) } + let(:bob) { Fabricate(:account, username: 'bob').tap(&:save!) } - it 'returns true if any keyword in the set matches the status text' do - status.update_attribute(:text, 'This is a hot take') + describe '.matcher_for' do + let(:matcher) { KeywordMute.matcher_for(alice) } - expect(KeywordMute.where(account: alice).matches?(status.text)).to be_truthy + describe 'with no KeywordMutes for an account' do + before do + KeywordMute.delete_all + end + + it 'does not match' do + expect(matcher =~ 'This is a hot take').to be_falsy + end end - it 'returns false if no keyword in the set matches the status text' + describe 'with KeywordMutes for an account' do + it 'does not match keywords set by a different account' do + 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 + KeywordMute.create!(account: alice, keyword: 'cold') + + expect(matcher =~ 'This is a hot take').to be_falsy + end + + it 'does not match substrings matching keywords' do + KeywordMute.create!(account: alice, keyword: 'take') + + expect(matcher =~ 'This is a shiitake mushroom').to be_falsy + end + + it 'matches keywords at the beginning of the text' do + KeywordMute.create!(account: alice, keyword: 'take') + + expect(matcher =~ 'Take this').to be_truthy + end + + it 'matches keywords at the beginning of the text' do + 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 + KeywordMute.create!(account: alice, keyword: 'hot') + + expect(matcher =~ 'This is a hot take').to be_truthy + end + + it 'uses case-folding rules appropriate for more than just English' do + 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 + KeywordMute.create!(account: alice, keyword: 'a shiitake') - describe 'matching' do - it 'is case-insensitive' + expect(matcher =~ 'This is a shiitake').to be_truthy + expect(matcher =~ 'This is shiitake').to_not be_truthy + end end end end -- cgit From 670e6a33f8eeca628707dc020e02ce32502d74a4 Mon Sep 17 00:00:00 2001 From: David Yip Date: Sat, 21 Oct 2017 14:47:17 -0500 Subject: Move KeywordMute into Glitch namespace. There are two motivations for this: 1. It looks like we're going to add other features that require server-side storage (e.g. user notes). 2. Namespacing glitchsoc modifications is a good idea anyway: even if we do not end up doing (1), if upstream introduces a keyword-mute feature that also uses a "KeywordMute" model, we can avoid some merge conflicts this way and work on the more interesting task of choosing which implementation to use. --- .../settings/keyword_mutes_controller.rb | 2 +- app/lib/feed_manager.rb | 2 +- app/models/glitch.rb | 7 ++ app/models/glitch/keyword_mute.rb | 49 +++++++++++++ app/models/keyword_mute.rb | 49 ------------- ...900_move_keyword_mutes_into_glitch_namespace.rb | 7 ++ db/schema.rb | 22 +++--- spec/fabricators/glitch_keyword_mute_fabricator.rb | 2 + spec/fabricators/keyword_mute_fabricator.rb | 2 - spec/models/glitch/keyword_mute_spec.rb | 83 ++++++++++++++++++++++ spec/models/keyword_mute_spec.rb | 83 ---------------------- 11 files changed, 161 insertions(+), 147 deletions(-) create mode 100644 app/models/glitch.rb create mode 100644 app/models/glitch/keyword_mute.rb delete mode 100644 app/models/keyword_mute.rb create mode 100644 db/migrate/20171021191900_move_keyword_mutes_into_glitch_namespace.rb create mode 100644 spec/fabricators/glitch_keyword_mute_fabricator.rb delete mode 100644 spec/fabricators/keyword_mute_fabricator.rb create mode 100644 spec/models/glitch/keyword_mute_spec.rb delete mode 100644 spec/models/keyword_mute_spec.rb (limited to 'app/lib') diff --git a/app/controllers/settings/keyword_mutes_controller.rb b/app/controllers/settings/keyword_mutes_controller.rb index d9f99af09..6ae05108d 100644 --- a/app/controllers/settings/keyword_mutes_controller.rb +++ b/app/controllers/settings/keyword_mutes_controller.rb @@ -55,7 +55,7 @@ class Settings::KeywordMutesController < ApplicationController end def keyword_mutes_for_account - KeywordMute.where(account: @account) + Glitch::KeywordMute.where(account: @account) end def load_keyword_mute diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 516bd81af..1123f88bb 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -138,7 +138,7 @@ class FeedManager end def filter_from_home?(status, receiver_id) - return true if KeywordMute.matcher_for(receiver_id) =~ status.text + return true if Glitch::KeywordMute.matcher_for(receiver_id) =~ status.text 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?) diff --git a/app/models/glitch.rb b/app/models/glitch.rb new file mode 100644 index 000000000..0e497babc --- /dev/null +++ b/app/models/glitch.rb @@ -0,0 +1,7 @@ +# 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 new file mode 100644 index 000000000..3b0b47f52 --- /dev/null +++ b/app/models/glitch/keyword_mute.rb @@ -0,0 +1,49 @@ +# 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) + Rails.cache.fetch("keyword_mutes:matcher:#{account_id}") { Matcher.new(account_id) } + end + + private + + def invalidate_cached_matcher + Rails.cache.delete("keyword_mutes:matcher:#{account_id}") + end + + class Matcher + attr_reader :regex + + def initialize(account_id) + re = [].tap do |arr| + Glitch::KeywordMute.where(account_id: account_id).select(:keyword, :id, :whole_word).find_each do |m| + boundary = m.whole_word ? '\b' : '' + arr << "#{boundary}#{Regexp.escape(m.keyword.strip)}#{boundary}" + end + end.join('|') + + @regex = /#{re}/i unless re.empty? + end + + def =~(str) + regex ? regex =~ str : false + end + end +end diff --git a/app/models/keyword_mute.rb b/app/models/keyword_mute.rb deleted file mode 100644 index b0229923d..000000000 --- a/app/models/keyword_mute.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true -# == Schema Information -# -# Table name: 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 KeywordMute < ApplicationRecord - belongs_to :account, required: true - - validates_presence_of :keyword - - after_commit :invalidate_cached_matcher - - def self.matcher_for(account_id) - Rails.cache.fetch("keyword_mutes:matcher:#{account_id}") { Matcher.new(account_id) } - end - - private - - def invalidate_cached_matcher - Rails.cache.delete("keyword_mutes:matcher:#{account_id}") - end - - class Matcher - attr_reader :regex - - def initialize(account_id) - re = [].tap do |arr| - KeywordMute.where(account_id: account_id).select(:keyword, :id, :whole_word).find_each do |m| - boundary = m.whole_word ? '\b' : '' - arr << "#{boundary}#{Regexp.escape(m.keyword.strip)}#{boundary}" - end - end.join('|') - - @regex = /#{re}/i unless re.empty? - end - - def =~(str) - regex ? regex =~ str : false - end - end -end diff --git a/db/migrate/20171021191900_move_keyword_mutes_into_glitch_namespace.rb b/db/migrate/20171021191900_move_keyword_mutes_into_glitch_namespace.rb new file mode 100644 index 000000000..269bb49d6 --- /dev/null +++ b/db/migrate/20171021191900_move_keyword_mutes_into_glitch_namespace.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index c0704b13e..c09876c4d 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: 20171010025614) do +ActiveRecord::Schema.define(version: 20171021191900) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -155,6 +155,15 @@ ActiveRecord::Schema.define(version: 20171010025614) 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.index ["account_id"], name: "index_glitch_keyword_mutes_on_account_id" + end + create_table "imports", force: :cascade do |t| t.integer "type", null: false t.boolean "approved", default: false, null: false @@ -167,15 +176,6 @@ ActiveRecord::Schema.define(version: 20171010025614) do t.bigint "account_id", null: false end - create_table "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.index ["account_id"], name: "index_keyword_mutes_on_account_id" - end - create_table "media_attachments", force: :cascade do |t| t.bigint "status_id" t.string "file_file_name" @@ -481,8 +481,8 @@ ActiveRecord::Schema.define(version: 20171010025614) 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 "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade - add_foreign_key "keyword_mutes", "accounts", on_delete: :cascade add_foreign_key "media_attachments", "accounts", name: "fk_96dd81e81b", on_delete: :nullify add_foreign_key "media_attachments", "statuses", on_delete: :nullify add_foreign_key "mentions", "accounts", name: "fk_970d43f9d1", on_delete: :cascade diff --git a/spec/fabricators/glitch_keyword_mute_fabricator.rb b/spec/fabricators/glitch_keyword_mute_fabricator.rb new file mode 100644 index 000000000..8601ed6d7 --- /dev/null +++ b/spec/fabricators/glitch_keyword_mute_fabricator.rb @@ -0,0 +1,2 @@ +Fabricator(:glitch_keyword_mute) do +end diff --git a/spec/fabricators/keyword_mute_fabricator.rb b/spec/fabricators/keyword_mute_fabricator.rb deleted file mode 100644 index 82cf845c8..000000000 --- a/spec/fabricators/keyword_mute_fabricator.rb +++ /dev/null @@ -1,2 +0,0 @@ -Fabricator(:keyword_mute) do -end diff --git a/spec/models/glitch/keyword_mute_spec.rb b/spec/models/glitch/keyword_mute_spec.rb new file mode 100644 index 000000000..108cdafec --- /dev/null +++ b/spec/models/glitch/keyword_mute_spec.rb @@ -0,0 +1,83 @@ +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 Glitch::KeywordMutes for an account' 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 Glitch::KeywordMutes for an account' 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 beginning 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 =~ 'This is a ~*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 diff --git a/spec/models/keyword_mute_spec.rb b/spec/models/keyword_mute_spec.rb deleted file mode 100644 index c74505188..000000000 --- a/spec/models/keyword_mute_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'rails_helper' - -RSpec.describe 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) { KeywordMute.matcher_for(alice) } - - describe 'with no KeywordMutes for an account' do - before do - KeywordMute.delete_all - end - - it 'does not match' do - expect(matcher =~ 'This is a hot take').to be_falsy - end - end - - describe 'with KeywordMutes for an account' do - it 'does not match keywords set by a different account' do - 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 - KeywordMute.create!(account: alice, keyword: 'cold') - - expect(matcher =~ 'This is a hot take').to be_falsy - end - - it 'considers word boundaries when matching' do - 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 - 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 - KeywordMute.create!(account: alice, keyword: 'take') - - expect(matcher =~ 'Take this').to be_truthy - end - - it 'matches keywords at the beginning of the text' do - 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 - 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 - KeywordMute.create!(account: alice, keyword: 'hot') - - expect(matcher =~ 'This is a ~*HOT*~ take').to be_truthy - end - - it 'uses case-folding rules appropriate for more than just English' do - 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 - 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 -- cgit From ad86c86fa8e0d577b1a6c7411367420e6beea4ea Mon Sep 17 00:00:00 2001 From: David Yip Date: Sat, 21 Oct 2017 15:44:47 -0500 Subject: Apply keyword mutes to reblogs. --- app/lib/feed_manager.rb | 5 ++++- app/models/glitch/keyword_mute.rb | 4 ++++ spec/fabricators/glitch_keyword_mute_fabricator.rb | 2 +- spec/lib/feed_manager_spec.rb | 17 +++++++++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) (limited to 'app/lib') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 1123f88bb..576188324 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -138,7 +138,9 @@ class FeedManager end def filter_from_home?(status, receiver_id) - return true if Glitch::KeywordMute.matcher_for(receiver_id) =~ status.text + keyword_mute_matcher = Glitch::KeywordMute.matcher_for(receiver_id) + + return true if keyword_mute_matcher =~ status.text 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?) @@ -161,6 +163,7 @@ class FeedManager return should_filter elsif status.reblog? # Filter out a reblog should_filter = Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists? # or if the author of the reblogged status is blocking me + should_filter ||= keyword_mute_matcher.matches?(status.reblog.text) should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists? # or the author's domain is blocked return should_filter end diff --git a/app/models/glitch/keyword_mute.rb b/app/models/glitch/keyword_mute.rb index 3b0b47f52..823e252d3 100644 --- a/app/models/glitch/keyword_mute.rb +++ b/app/models/glitch/keyword_mute.rb @@ -45,5 +45,9 @@ class Glitch::KeywordMute < ApplicationRecord def =~(str) regex ? regex =~ str : false end + + def matches?(str) + !!(regex =~ str) + end end end diff --git a/spec/fabricators/glitch_keyword_mute_fabricator.rb b/spec/fabricators/glitch_keyword_mute_fabricator.rb index 8601ed6d7..20d393320 100644 --- a/spec/fabricators/glitch_keyword_mute_fabricator.rb +++ b/spec/fabricators/glitch_keyword_mute_fabricator.rb @@ -1,2 +1,2 @@ -Fabricator(:glitch_keyword_mute) do +Fabricator('Glitch::KeywordMute') do end diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 1861cc6ed..c9403d616 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -119,6 +119,23 @@ 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 for a status containing a muted keyword' do + Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take') + alice.follow!(bob) + 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 reblog containing a muted keyword' do + Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take') + alice.follow!(jeff) + 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 end context 'for mentions feed' do -- cgit From 19826774f06244b0c84a1973b3a366df0d7f0f5a Mon Sep 17 00:00:00 2001 From: David Yip Date: Sun, 22 Oct 2017 00:23:21 -0500 Subject: keyword mutes: also check spoiler (CW) text and reblogged statuses. --- app/lib/feed_manager.rb | 19 ++++++++++++++----- spec/lib/feed_manager_spec.rb | 25 +++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 7 deletions(-) (limited to 'app/lib') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 576188324..e0a257cd0 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -138,13 +138,11 @@ class FeedManager end def filter_from_home?(status, receiver_id) - keyword_mute_matcher = Glitch::KeywordMute.matcher_for(receiver_id) - - return true if keyword_mute_matcher =~ status.text - 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?(status, Glitch::KeywordMute.matcher_for(receiver_id)) + check_for_mutes = [status.account_id] check_for_mutes.concat(status.mentions.pluck(:account_id)) check_for_mutes.concat([status.reblog.account_id]) if status.reblog? @@ -163,7 +161,6 @@ class FeedManager return should_filter elsif status.reblog? # Filter out a reblog should_filter = Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists? # or if the author of the reblogged status is blocking me - should_filter ||= keyword_mute_matcher.matches?(status.reblog.text) should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists? # or the author's domain is blocked return should_filter end @@ -171,6 +168,18 @@ class FeedManager false end + def keyword_filter?(status, matcher) + should_filter = matcher =~ status.text + should_filter ||= matcher =~ status.spoiler_text + + if status.reblog? + should_filter ||= matcher =~ status.reblog.text + should_filter ||= matcher =~ status.reblog.spoiler_text + end + + should_filter + end + def filter_from_mentions?(status, receiver_id) return true if receiver_id == status.account_id diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index c9403d616..23ce373f2 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -122,20 +122,41 @@ RSpec.describe FeedManager do it 'returns true for a status containing a muted keyword' do Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take') - alice.follow!(bob) 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') - alice.follow!(jeff) 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 end context 'for mentions feed' do -- cgit From d03b48cea06ac87eecaf7eae96d175ab8ee621ca Mon Sep 17 00:00:00 2001 From: David Yip Date: Tue, 24 Oct 2017 18:51:27 -0500 Subject: Also filter notifications containing muted keywords. --- app/lib/feed_manager.rb | 3 ++- spec/lib/feed_manager_spec.rb | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'app/lib') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index e0a257cd0..2ddfac336 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -177,7 +177,7 @@ class FeedManager should_filter ||= matcher =~ status.reblog.spoiler_text end - should_filter + !!should_filter end def filter_from_mentions?(status, receiver_id) @@ -189,6 +189,7 @@ class FeedManager should_filter = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any? # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked 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, Glitch::KeywordMute.matcher_for(receiver_id)) # or if the mention contains a muted keyword should_filter end diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 23ce373f2..e678d3ca4 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -185,6 +185,13 @@ 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 end end -- cgit From 9a42f7cbed28ca010eca6cef89298ab806ce298f Mon Sep 17 00:00:00 2001 From: aschmitz Date: Sat, 11 Nov 2017 22:10:49 -0600 Subject: Actually filter blocked reblogs from feed And even a relevant test. Whoops. --- app/lib/feed_manager.rb | 4 +++- spec/lib/feed_manager_spec.rb | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) (limited to 'app/lib') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 2ddfac336..3b16b5d52 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -160,7 +160,9 @@ class FeedManager should_filter &&= status.account_id != status.in_reply_to_account_id # and it's not a self-reply return should_filter elsif status.reblog? # Filter out a reblog - should_filter = Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists? # or if the author of the reblogged status is blocking me + src_id = status.account_id + should_filter = Follow.where(account_id: receiver_id, target_account_id: src_id, show_reblogs: false).exists? # if the reblogger's reblogs are suppressed + should_filter ||= Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists? # or if the author of the reblogged status is blocking me should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists? # or the author's domain is blocked return should_filter end diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index e678d3ca4..715d85306 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -56,6 +56,13 @@ RSpec.describe FeedManager do expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true end + it 'returns true for reblog from account with reblogs disabled' do + status = Fabricate(:status, text: 'Hello world', account: jeff) + reblog = Fabricate(:status, reblog: status, account: alice) + bob.follow!(alice, reblogs: false) + expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true + end + it 'returns false for reply by followee to another followee' do status = Fabricate(:status, text: 'Hello world', account: jeff) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) -- cgit