From a29a982eaa0536a741b43ffb3397c74e3abe7196 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 24 Feb 2022 17:28:23 +0100 Subject: Change e-mail domain blocks to block IPs dynamically (#17635) * Change e-mail domain blocks to block IPs dynamically * Update app/workers/scheduler/email_domain_block_refresh_scheduler.rb Co-authored-by: Yamagishi Kazutoshi * Update app/workers/scheduler/email_domain_block_refresh_scheduler.rb Co-authored-by: Yamagishi Kazutoshi Co-authored-by: Yamagishi Kazutoshi --- .../admin/email_domain_blocks_controller_spec.rb | 42 +++++++++++----------- 1 file changed, 21 insertions(+), 21 deletions(-) (limited to 'spec/controllers') diff --git a/spec/controllers/admin/email_domain_blocks_controller_spec.rb b/spec/controllers/admin/email_domain_blocks_controller_spec.rb index 52db56f4e..cf194579d 100644 --- a/spec/controllers/admin/email_domain_blocks_controller_spec.rb +++ b/spec/controllers/admin/email_domain_blocks_controller_spec.rb @@ -17,43 +17,43 @@ RSpec.describe Admin::EmailDomainBlocksController, type: :controller do EmailDomainBlock.paginates_per default_per_page end - it 'renders email blacks' do + it 'returns http success' do 2.times { Fabricate(:email_domain_block) } - get :index, params: { page: 2 } - - assigned = assigns(:email_domain_blocks) - expect(assigned.count).to eq 1 - expect(assigned.klass).to be EmailDomainBlock expect(response).to have_http_status(200) end end describe 'GET #new' do - it 'assigns a new email black' do + it 'returns http success' do get :new - - expect(assigns(:email_domain_block)).to be_instance_of(EmailDomainBlock) expect(response).to have_http_status(200) end end describe 'POST #create' do - it 'blocks the domain when succeeded to save' do - post :create, params: { email_domain_block: { domain: 'example.com' } } - - expect(flash[:notice]).to eq I18n.t('admin.email_domain_blocks.created_msg') - expect(response).to redirect_to(admin_email_domain_blocks_path) + context 'when resolve button is pressed' do + before do + post :create, params: { email_domain_block: { domain: 'example.com' } } + end + + it 'renders new template' do + expect(response).to render_template(:new) + end end - end - describe 'DELETE #destroy' do - it 'unblocks the domain' do - email_domain_block = Fabricate(:email_domain_block) - delete :destroy, params: { id: email_domain_block.id } + context 'when save button is pressed' do + before do + post :create, params: { email_domain_block: { domain: 'example.com' }, save: '' } + end + + it 'blocks the domain' do + expect(EmailDomainBlock.find_by(domain: 'example.com')).to_not be_nil + end - expect(flash[:notice]).to eq I18n.t('admin.email_domain_blocks.destroyed_msg') - expect(response).to redirect_to(admin_email_domain_blocks_path) + it 'redirects to e-mail domain blocks' do + expect(response).to redirect_to(admin_email_domain_blocks_path) + end end end end -- cgit From 27965ce5edff20db2de1dd233c88f8393bb0da0b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 25 Feb 2022 00:34:14 +0100 Subject: Add trending statuses (#17431) * Add trending statuses * Fix dangling items with stale scores in localized sets * Various fixes and improvements - Change approve_all/reject_all to approve_accounts/reject_accounts - Change Trends::Query methods to not mutate the original query - Change Trends::Query#skip to offset - Change follow recommendations to be refreshed in a transaction * Add tests for trending statuses filtering behaviour * Fix not applying filtering scope in controller --- .rubocop.yml | 5 +- .../links/preview_card_providers_controller.rb | 12 +- app/controllers/admin/trends/links_controller.rb | 20 +-- .../admin/trends/statuses_controller.rb | 45 +++++++ app/controllers/admin/trends/tags_controller.rb | 12 +- .../api/v1/admin/trends/links_controller.rb | 19 +++ .../api/v1/admin/trends/statuses_controller.rb | 19 +++ .../api/v1/admin/trends/tags_controller.rb | 2 +- app/controllers/api/v1/trends/links_controller.rb | 6 +- .../api/v1/trends/statuses_controller.rb | 27 ++++ app/controllers/api/v1/trends/tags_controller.rb | 2 +- app/controllers/concerns/localized.rb | 4 + app/helpers/admin/filter_helper.rb | 7 +- app/helpers/languages_helper.rb | 2 +- app/javascript/styles/mastodon/accounts.scss | 10 +- app/javascript/styles/mastodon/tables.scss | 7 + app/lib/activitypub/activity/announce.rb | 3 +- app/lib/activitypub/activity/like.rb | 2 + app/mailers/admin_mailer.rb | 27 ++-- app/models/account.rb | 30 +++-- app/models/form/preview_card_batch.rb | 65 ---------- app/models/form/preview_card_provider_batch.rb | 33 ----- app/models/form/tag_batch.rb | 37 ------ app/models/preview_card_filter.rb | 53 -------- app/models/preview_card_provider_filter.rb | 49 ------- app/models/status.rb | 12 ++ app/models/tag_filter.rb | 66 ---------- app/models/trends.rb | 26 +++- app/models/trends/base.rb | 20 ++- app/models/trends/links.rb | 52 ++++---- app/models/trends/preview_card_batch.rb | 65 ++++++++++ app/models/trends/preview_card_filter.rb | 46 +++++++ app/models/trends/preview_card_provider_batch.rb | 33 +++++ app/models/trends/preview_card_provider_filter.rb | 49 +++++++ app/models/trends/query.rb | 106 +++++++++++++++ app/models/trends/status_batch.rb | 65 ++++++++++ app/models/trends/status_filter.rb | 46 +++++++ app/models/trends/statuses.rb | 142 +++++++++++++++++++++ app/models/trends/tag_batch.rb | 37 ++++++ app/models/trends/tag_filter.rb | 60 +++++++++ app/models/trends/tags.rb | 36 ++---- app/models/user.rb | 2 +- app/policies/account_policy.rb | 4 + app/policies/preview_card_policy.rb | 2 +- app/policies/preview_card_provider_policy.rb | 2 +- app/policies/status_policy.rb | 4 + app/policies/tag_policy.rb | 4 + app/services/delete_account_service.rb | 32 ++--- app/services/favourite_service.rb | 2 + app/services/reblog_service.rb | 3 +- .../admin/custom_emojis/_custom_emoji.html.haml | 2 +- .../admin/follow_recommendations/show.html.haml | 6 +- app/views/admin/trends/links/index.html.haml | 34 +++-- .../links/preview_card_providers/index.html.haml | 2 +- app/views/admin/trends/statuses/_status.html.haml | 30 +++++ app/views/admin/trends/statuses/index.html.haml | 43 +++++++ app/views/admin/trends/tags/index.html.haml | 4 +- .../admin_mailer/_new_trending_links.text.erb | 14 ++ .../admin_mailer/_new_trending_statuses.text.erb | 14 ++ app/views/admin_mailer/_new_trending_tags.text.erb | 14 ++ app/views/admin_mailer/new_trending_links.text.erb | 16 --- app/views/admin_mailer/new_trending_tags.text.erb | 16 --- app/views/admin_mailer/new_trends.text.erb | 13 ++ app/views/application/_sidebar.html.haml | 2 +- .../scheduler/follow_recommendations_scheduler.rb | 8 +- config/brakeman.ignore | 68 ++++------ config/locales/en.yml | 34 +++-- config/navigation.rb | 1 + config/routes.rb | 9 ++ .../20220202200743_add_trendable_to_accounts.rb | 7 + .../20220202200926_add_trendable_to_statuses.rb | 5 + ...20202201015_remove_trust_level_from_accounts.rb | 9 ++ db/schema.rb | 6 +- .../api/v1/trends/tags_controller_spec.rb | 7 +- spec/mailers/previews/admin_mailer_preview.rb | 11 +- spec/models/trends/statuses_spec.rb | 110 ++++++++++++++++ spec/models/trends/tags_spec.rb | 6 +- 77 files changed, 1336 insertions(+), 569 deletions(-) create mode 100644 app/controllers/admin/trends/statuses_controller.rb create mode 100644 app/controllers/api/v1/admin/trends/links_controller.rb create mode 100644 app/controllers/api/v1/admin/trends/statuses_controller.rb create mode 100644 app/controllers/api/v1/trends/statuses_controller.rb delete mode 100644 app/models/form/preview_card_batch.rb delete mode 100644 app/models/form/preview_card_provider_batch.rb delete mode 100644 app/models/form/tag_batch.rb delete mode 100644 app/models/preview_card_filter.rb delete mode 100644 app/models/preview_card_provider_filter.rb delete mode 100644 app/models/tag_filter.rb create mode 100644 app/models/trends/preview_card_batch.rb create mode 100644 app/models/trends/preview_card_filter.rb create mode 100644 app/models/trends/preview_card_provider_batch.rb create mode 100644 app/models/trends/preview_card_provider_filter.rb create mode 100644 app/models/trends/query.rb create mode 100644 app/models/trends/status_batch.rb create mode 100644 app/models/trends/status_filter.rb create mode 100644 app/models/trends/statuses.rb create mode 100644 app/models/trends/tag_batch.rb create mode 100644 app/models/trends/tag_filter.rb create mode 100644 app/views/admin/trends/statuses/_status.html.haml create mode 100644 app/views/admin/trends/statuses/index.html.haml create mode 100644 app/views/admin_mailer/_new_trending_links.text.erb create mode 100644 app/views/admin_mailer/_new_trending_statuses.text.erb create mode 100644 app/views/admin_mailer/_new_trending_tags.text.erb delete mode 100644 app/views/admin_mailer/new_trending_links.text.erb delete mode 100644 app/views/admin_mailer/new_trending_tags.text.erb create mode 100644 app/views/admin_mailer/new_trends.text.erb create mode 100644 db/migrate/20220202200743_add_trendable_to_accounts.rb create mode 100644 db/migrate/20220202200926_add_trendable_to_statuses.rb create mode 100644 db/post_migrate/20220202201015_remove_trust_level_from_accounts.rb create mode 100644 spec/models/trends/statuses_spec.rb (limited to 'spec/controllers') diff --git a/.rubocop.yml b/.rubocop.yml index 2af0f59bb..68634e9e3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -32,10 +32,11 @@ Layout/EmptyLineAfterGuardClause: Layout/EmptyLinesAroundAttributeAccessor: Enabled: true +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent + Layout/HashAlignment: Enabled: false - # EnforcedHashRocketStyle: table - # EnforcedColonStyle: table Layout/SpaceAroundMethodCallOperator: Enabled: true diff --git a/app/controllers/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/admin/trends/links/preview_card_providers_controller.rb index 2c26e03f3..40a466cd6 100644 --- a/app/controllers/admin/trends/links/preview_card_providers_controller.rb +++ b/app/controllers/admin/trends/links/preview_card_providers_controller.rb @@ -5,11 +5,11 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll authorize :preview_card_provider, :index? @preview_card_providers = filtered_preview_card_providers.page(params[:page]) - @form = Form::PreviewCardProviderBatch.new + @form = Trends::PreviewCardProviderBatch.new end def batch - @form = Form::PreviewCardProviderBatch.new(form_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button)) + @form = Trends::PreviewCardProviderBatch.new(trends_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button)) @form.save rescue ActionController::ParameterMissing flash[:alert] = I18n.t('admin.accounts.no_account_selected') @@ -20,15 +20,15 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll private def filtered_preview_card_providers - PreviewCardProviderFilter.new(filter_params).results + Trends::PreviewCardProviderFilter.new(filter_params).results end def filter_params - params.slice(:page, *PreviewCardProviderFilter::KEYS).permit(:page, *PreviewCardProviderFilter::KEYS) + params.slice(:page, *Trends::PreviewCardProviderFilter::KEYS).permit(:page, *Trends::PreviewCardProviderFilter::KEYS) end - def form_preview_card_provider_batch_params - params.require(:form_preview_card_provider_batch).permit(:action, preview_card_provider_ids: []) + def trends_preview_card_provider_batch_params + params.require(:trends_preview_card_provider_batch).permit(:action, preview_card_provider_ids: []) end def action_from_button diff --git a/app/controllers/admin/trends/links_controller.rb b/app/controllers/admin/trends/links_controller.rb index 619b37deb..434eec5fe 100644 --- a/app/controllers/admin/trends/links_controller.rb +++ b/app/controllers/admin/trends/links_controller.rb @@ -5,11 +5,11 @@ class Admin::Trends::LinksController < Admin::BaseController authorize :preview_card, :index? @preview_cards = filtered_preview_cards.page(params[:page]) - @form = Form::PreviewCardBatch.new + @form = Trends::PreviewCardBatch.new end def batch - @form = Form::PreviewCardBatch.new(form_preview_card_batch_params.merge(current_account: current_account, action: action_from_button)) + @form = Trends::PreviewCardBatch.new(trends_preview_card_batch_params.merge(current_account: current_account, action: action_from_button)) @form.save rescue ActionController::ParameterMissing flash[:alert] = I18n.t('admin.accounts.no_account_selected') @@ -20,26 +20,26 @@ class Admin::Trends::LinksController < Admin::BaseController private def filtered_preview_cards - PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results + Trends::PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results end def filter_params - params.slice(:page, *PreviewCardFilter::KEYS).permit(:page, *PreviewCardFilter::KEYS) + params.slice(:page, *Trends::PreviewCardFilter::KEYS).permit(:page, *Trends::PreviewCardFilter::KEYS) end - def form_preview_card_batch_params - params.require(:form_preview_card_batch).permit(:action, preview_card_ids: []) + def trends_preview_card_batch_params + params.require(:trends_preview_card_batch).permit(:action, preview_card_ids: []) end def action_from_button if params[:approve] 'approve' - elsif params[:approve_all] - 'approve_all' + elsif params[:approve_providers] + 'approve_providers' elsif params[:reject] 'reject' - elsif params[:reject_all] - 'reject_all' + elsif params[:reject_providers] + 'reject_providers' end end end diff --git a/app/controllers/admin/trends/statuses_controller.rb b/app/controllers/admin/trends/statuses_controller.rb new file mode 100644 index 000000000..766242738 --- /dev/null +++ b/app/controllers/admin/trends/statuses_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class Admin::Trends::StatusesController < Admin::BaseController + def index + authorize :status, :index? + + @statuses = filtered_statuses.page(params[:page]) + @form = Trends::StatusBatch.new + end + + def batch + @form = Trends::StatusBatch.new(trends_status_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.accounts.no_account_selected') + ensure + redirect_to admin_trends_statuses_path(filter_params) + end + + private + + def filtered_statuses + Trends::StatusFilter.new(filter_params.with_defaults(trending: 'all')).results.includes(:account, :media_attachments, :active_mentions) + end + + def filter_params + params.slice(:page, *Trends::StatusFilter::KEYS).permit(:page, *Trends::StatusFilter::KEYS) + end + + def trends_status_batch_params + params.require(:trends_status_batch).permit(:action, status_ids: []) + end + + def action_from_button + if params[:approve] + 'approve' + elsif params[:approve_accounts] + 'approve_accounts' + elsif params[:reject] + 'reject' + elsif params[:reject_accounts] + 'reject_accounts' + end + end +end diff --git a/app/controllers/admin/trends/tags_controller.rb b/app/controllers/admin/trends/tags_controller.rb index 91ff33d40..f4d1ec0d1 100644 --- a/app/controllers/admin/trends/tags_controller.rb +++ b/app/controllers/admin/trends/tags_controller.rb @@ -5,11 +5,11 @@ class Admin::Trends::TagsController < Admin::BaseController authorize :tag, :index? @tags = filtered_tags.page(params[:page]) - @form = Form::TagBatch.new + @form = Trends::TagBatch.new end def batch - @form = Form::TagBatch.new(form_tag_batch_params.merge(current_account: current_account, action: action_from_button)) + @form = Trends::TagBatch.new(trends_tag_batch_params.merge(current_account: current_account, action: action_from_button)) @form.save rescue ActionController::ParameterMissing flash[:alert] = I18n.t('admin.accounts.no_account_selected') @@ -20,15 +20,15 @@ class Admin::Trends::TagsController < Admin::BaseController private def filtered_tags - TagFilter.new(filter_params).results + Trends::TagFilter.new(filter_params).results end def filter_params - params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS) + params.slice(:page, *Trends::TagFilter::KEYS).permit(:page, *Trends::TagFilter::KEYS) end - def form_tag_batch_params - params.require(:form_tag_batch).permit(:action, tag_ids: []) + def trends_tag_batch_params + params.require(:trends_tag_batch).permit(:action, tag_ids: []) end def action_from_button diff --git a/app/controllers/api/v1/admin/trends/links_controller.rb b/app/controllers/api/v1/admin/trends/links_controller.rb new file mode 100644 index 000000000..63b3d9358 --- /dev/null +++ b/app/controllers/api/v1/admin/trends/links_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Api::V1::Admin::Trends::LinksController < Api::BaseController + protect_from_forgery with: :exception + + before_action -> { authorize_if_got_token! :'admin:read' } + before_action :require_staff! + before_action :set_links + + def index + render json: @links, each_serializer: REST::Trends::LinkSerializer + end + + private + + def set_links + @links = Trends.links.query.limit(limit_param(10)) + end +end diff --git a/app/controllers/api/v1/admin/trends/statuses_controller.rb b/app/controllers/api/v1/admin/trends/statuses_controller.rb new file mode 100644 index 000000000..86633cc74 --- /dev/null +++ b/app/controllers/api/v1/admin/trends/statuses_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Api::V1::Admin::Trends::StatusesController < Api::BaseController + protect_from_forgery with: :exception + + before_action -> { authorize_if_got_token! :'admin:read' } + before_action :require_staff! + before_action :set_statuses + + def index + render json: @statuses, each_serializer: REST::StatusSerializer + end + + private + + def set_statuses + @statuses = cache_collection(Trends.statuses.query.limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status) + end +end diff --git a/app/controllers/api/v1/admin/trends/tags_controller.rb b/app/controllers/api/v1/admin/trends/tags_controller.rb index 4815af31e..5cc4c269d 100644 --- a/app/controllers/api/v1/admin/trends/tags_controller.rb +++ b/app/controllers/api/v1/admin/trends/tags_controller.rb @@ -14,6 +14,6 @@ class Api::V1::Admin::Trends::TagsController < Api::BaseController private def set_tags - @tags = Trends.tags.get(false, limit_param(10)) + @tags = Trends.tags.query.limit(limit_param(10)) end end diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb index 1c3ab1e1c..ad20e7f8b 100644 --- a/app/controllers/api/v1/trends/links_controller.rb +++ b/app/controllers/api/v1/trends/links_controller.rb @@ -12,10 +12,14 @@ class Api::V1::Trends::LinksController < Api::BaseController def set_links @links = begin if Setting.trends - Trends.links.get(true, limit_param(10)) + links_from_trends else [] end end end + + def links_from_trends + Trends.links.query.allowed.in_locale(content_locale).limit(limit_param(10)) + end end diff --git a/app/controllers/api/v1/trends/statuses_controller.rb b/app/controllers/api/v1/trends/statuses_controller.rb new file mode 100644 index 000000000..d4ec97ae5 --- /dev/null +++ b/app/controllers/api/v1/trends/statuses_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class Api::V1::Trends::StatusesController < Api::BaseController + before_action :set_statuses + + def index + render json: @statuses, each_serializer: REST::StatusSerializer + end + + private + + def set_statuses + @statuses = begin + if Setting.trends + cache_collection(statuses_from_trends, Status) + else + [] + end + end + end + + def statuses_from_trends + scope = Trends.statuses.query.allowed.in_locale(content_locale) + scope = scope.filtered_for(current_account) if user_signed_in? + scope.limit(limit_param(DEFAULT_STATUSES_LIMIT)) + end +end diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index 947b53de2..1334b72d2 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -12,7 +12,7 @@ class Api::V1::Trends::TagsController < Api::BaseController def set_tags @tags = begin if Setting.trends - Trends.tags.get(true, limit_param(10)) + Trends.tags.query.allowed.limit(limit_param(10)) else [] end diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb index 173316800..ede299d5a 100644 --- a/app/controllers/concerns/localized.rb +++ b/app/controllers/concerns/localized.rb @@ -27,4 +27,8 @@ module Localized def available_locale_or_nil(locale_name) locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym) end + + def content_locale + @content_locale ||= I18n.locale.to_s.split(/[_-]/).first + end end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 907529b37..140fc73ed 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -5,9 +5,10 @@ module Admin::FilterHelper AccountFilter::KEYS, CustomEmojiFilter::KEYS, ReportFilter::KEYS, - TagFilter::KEYS, - PreviewCardProviderFilter::KEYS, - PreviewCardFilter::KEYS, + Trends::TagFilter::KEYS, + Trends::PreviewCardProviderFilter::KEYS, + Trends::PreviewCardFilter::KEYS, + Trends::StatusFilter::KEYS, InstanceFilter::KEYS, InviteFilter::KEYS, RelationshipFilter::KEYS, diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index 3a65af686..f22cc6d28 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -242,6 +242,6 @@ module LanguagesHelper end def valid_locale?(locale) - SUPPORTED_LOCALES.key?(locale.to_sym) + locale.present? && SUPPORTED_LOCALES.key?(locale.to_sym) end end diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index 485fe4a9d..215774a19 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -331,7 +331,8 @@ } .batch-table__row--muted .pending-account__header, -.batch-table__row--muted .accounts-table { +.batch-table__row--muted .accounts-table, +.batch-table__row--muted .name-tag { &, a, strong { @@ -339,6 +340,10 @@ } } +.batch-table__row--muted .name-tag .avatar { + opacity: 0.5; +} + .batch-table__row--muted .accounts-table { tbody td.accounts-table__extra, &__count, @@ -352,7 +357,8 @@ } .batch-table__row--attention .pending-account__header, -.batch-table__row--attention .accounts-table { +.batch-table__row--attention .accounts-table, +.batch-table__row--attention .name-tag { &, a, strong { diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 36bc07a72..1f7e71776 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -210,6 +210,7 @@ a.table-action-link { &__content { padding-top: 12px; padding-bottom: 16px; + overflow: hidden; &--unpadded { padding: 0; @@ -296,3 +297,9 @@ a.table-action-link { } } } + +.one-liner { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index 12fad8da4..7cd5a41e8 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -23,8 +23,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity visibility: visibility_from_audience ) - Trends.tags.register(@status) - Trends.links.register(@status) + Trends.register!(@status) distribute end diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index c065f01f8..ebbda15b9 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -7,6 +7,8 @@ class ActivityPub::Activity::Like < ActivityPub::Activity return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.favourited?(original_status) favourite = original_status.favourites.create!(account: @account) + NotifyService.new.call(original_status.account, :favourite, favourite) + Trends.statuses.register(original_status) end end diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index a9d00c000..f416977d8 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -35,25 +35,18 @@ class AdminMailer < ApplicationMailer end end - def new_trending_tags(recipient, tags) - @tags = tags - @me = recipient - @instance = Rails.configuration.x.local_domain - @lowest_trending_tag = Trends.tags.get(true, Trends.tags.options[:review_threshold]).last + def new_trends(recipient, links, tags, statuses) + @links = links + @lowest_trending_link = Trends.links.query.allowed.limit(Trends.links.options[:review_threshold]).last + @tags = tags + @lowest_trending_tag = Trends.tags.query.allowed.limit(Trends.tags.options[:review_threshold]).last + @statuses = statuses + @lowest_trending_status = Trends.statuses.query.allowed.limit(Trends.statuses.options[:review_threshold]).last + @me = recipient + @instance = Rails.configuration.x.local_domain locale_for_account(@me) do - mail to: @me.user_email, subject: I18n.t('admin_mailer.new_trending_tags.subject', instance: @instance) - end - end - - def new_trending_links(recipient, links) - @links = links - @me = recipient - @instance = Rails.configuration.x.local_domain - @lowest_trending_link = Trends.links.get(true, Trends.links.options[:review_threshold]).last - - locale_for_account(@me) do - mail to: @me.user_email, subject: I18n.t('admin_mailer.new_trending_links.subject', instance: @instance) + mail to: @me.user_email, subject: I18n.t('admin_mailer.new_trends.subject', instance: @instance) end end end diff --git a/app/models/account.rb b/app/models/account.rb index 2ad45feda..dfdf9045f 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -40,13 +40,15 @@ # also_known_as :string is an Array # silenced_at :datetime # suspended_at :datetime -# trust_level :integer # hide_collections :boolean # avatar_storage_schema_version :integer # header_storage_schema_version :integer # devices_url :string # suspension_origin :integer # sensitized_at :datetime +# trendable :boolean +# reviewed_at :datetime +# requested_review_at :datetime # class Account < ApplicationRecord @@ -56,6 +58,7 @@ class Account < ApplicationRecord remote_url salmon_url hub_url + trust_level ) USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i @@ -74,11 +77,6 @@ class Account < ApplicationRecord include DomainMaterializable include AccountMerging - TRUST_LEVELS = { - untrusted: 0, - trusted: 1, - }.freeze - enum protocol: [:ostatus, :activitypub] enum suspension_origin: [:local, :remote], _prefix: true @@ -202,10 +200,6 @@ class Account < ApplicationRecord last_webfingered_at.nil? || last_webfingered_at <= 1.day.ago end - def trust_level - self[:trust_level] || 0 - end - def refresh! ResolveAccountService.new.call(acct) unless local? end @@ -388,6 +382,22 @@ class Account < ApplicationRecord @synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/" end + def requires_review? + reviewed_at.nil? + end + + def reviewed? + reviewed_at.present? + end + + def requested_review? + requested_review_at.present? + end + + def requires_review_notification? + requires_review? && !requested_review? + end + class Field < ActiveModelSerializers::Model attributes :name, :value, :verified_at, :account diff --git a/app/models/form/preview_card_batch.rb b/app/models/form/preview_card_batch.rb deleted file mode 100644 index 5f6e6522a..000000000 --- a/app/models/form/preview_card_batch.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -class Form::PreviewCardBatch - include ActiveModel::Model - include Authorization - - attr_accessor :preview_card_ids, :action, :current_account, :precision - - def save - case action - when 'approve' - approve! - when 'approve_all' - approve_all! - when 'reject' - reject! - when 'reject_all' - reject_all! - end - end - - private - - def preview_cards - @preview_cards ||= PreviewCard.where(id: preview_card_ids) - end - - def preview_card_providers - @preview_card_providers ||= preview_cards.map(&:domain).uniq.map { |domain| PreviewCardProvider.matching_domain(domain) || PreviewCardProvider.new(domain: domain) } - end - - def approve! - preview_cards.each { |preview_card| authorize(preview_card, :update?) } - preview_cards.update_all(trendable: true) - end - - def approve_all! - preview_card_providers.each do |provider| - authorize(provider, :update?) - provider.update(trendable: true, reviewed_at: action_time) - end - - # Reset any individual overrides - preview_cards.update_all(trendable: nil) - end - - def reject! - preview_cards.each { |preview_card| authorize(preview_card, :update?) } - preview_cards.update_all(trendable: false) - end - - def reject_all! - preview_card_providers.each do |provider| - authorize(provider, :update?) - provider.update(trendable: false, reviewed_at: action_time) - end - - # Reset any individual overrides - preview_cards.update_all(trendable: nil) - end - - def action_time - @action_time ||= Time.now.utc - end -end diff --git a/app/models/form/preview_card_provider_batch.rb b/app/models/form/preview_card_provider_batch.rb deleted file mode 100644 index e6ab3d8fa..000000000 --- a/app/models/form/preview_card_provider_batch.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -class Form::PreviewCardProviderBatch - include ActiveModel::Model - include Authorization - - attr_accessor :preview_card_provider_ids, :action, :current_account - - def save - case action - when 'approve' - approve! - when 'reject' - reject! - end - end - - private - - def preview_card_providers - PreviewCardProvider.where(id: preview_card_provider_ids) - end - - def approve! - preview_card_providers.each { |provider| authorize(provider, :update?) } - preview_card_providers.update_all(trendable: true, reviewed_at: Time.now.utc) - end - - def reject! - preview_card_providers.each { |provider| authorize(provider, :update?) } - preview_card_providers.update_all(trendable: false, reviewed_at: Time.now.utc) - end -end diff --git a/app/models/form/tag_batch.rb b/app/models/form/tag_batch.rb deleted file mode 100644 index b9330745f..000000000 --- a/app/models/form/tag_batch.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -class Form::TagBatch - include ActiveModel::Model - include Authorization - - attr_accessor :tag_ids, :action, :current_account - - def save - case action - when 'approve' - approve! - when 'reject' - reject! - end - end - - private - - def tags - Tag.where(id: tag_ids) - end - - def approve! - tags.each { |tag| authorize(tag, :update?) } - tags.update_all(trendable: true, reviewed_at: action_time) - end - - def reject! - tags.each { |tag| authorize(tag, :update?) } - tags.update_all(trendable: false, reviewed_at: action_time) - end - - def action_time - @action_time ||= Time.now.utc - end -end diff --git a/app/models/preview_card_filter.rb b/app/models/preview_card_filter.rb deleted file mode 100644 index 8dda9989c..000000000 --- a/app/models/preview_card_filter.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -class PreviewCardFilter - KEYS = %i( - trending - ).freeze - - attr_reader :params - - def initialize(params) - @params = params - end - - def results - scope = PreviewCard.unscoped - - params.each do |key, value| - next if key.to_s == 'page' - - scope.merge!(scope_for(key, value.to_s.strip)) if value.present? - end - - scope - end - - private - - def scope_for(key, value) - case key.to_s - when 'trending' - trending_scope(value) - else - raise "Unknown filter: #{key}" - end - end - - def trending_scope(value) - ids = begin - case value.to_s - when 'allowed' - Trends.links.currently_trending_ids(true, -1) - else - Trends.links.currently_trending_ids(false, -1) - end - end - - if ids.empty? - PreviewCard.none - else - PreviewCard.joins("join unnest(array[#{ids.map(&:to_i).join(',')}]::integer[]) with ordinality as x (id, ordering) on preview_cards.id = x.id").order('x.ordering') - end - end -end diff --git a/app/models/preview_card_provider_filter.rb b/app/models/preview_card_provider_filter.rb deleted file mode 100644 index 1e90d3c9d..000000000 --- a/app/models/preview_card_provider_filter.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -class PreviewCardProviderFilter - KEYS = %i( - status - ).freeze - - attr_reader :params - - def initialize(params) - @params = params - end - - def results - scope = PreviewCardProvider.unscoped - - params.each do |key, value| - next if key.to_s == 'page' - - scope.merge!(scope_for(key, value.to_s.strip)) if value.present? - end - - scope.order(domain: :asc) - end - - private - - def scope_for(key, value) - case key.to_s - when 'status' - status_scope(value) - else - raise "Unknown filter: #{key}" - end - end - - def status_scope(value) - case value.to_s - when 'approved' - PreviewCardProvider.trendable - when 'rejected' - PreviewCardProvider.not_trendable - when 'pending_review' - PreviewCardProvider.pending_review - else - raise "Unknown status: #{value}" - end - end -end diff --git a/app/models/status.rb b/app/models/status.rb index 96e41b1d3..adb92ef91 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -268,6 +268,18 @@ class Status < ApplicationRecord update_status_stat!(key => [public_send(key) - 1, 0].max) end + def trendable? + if attributes['trendable'].nil? + account.trendable? + else + attributes['trendable'] + end + end + + def requires_review_notification? + attributes['trendable'].nil? && account.requires_review_notification? + end + after_create_commit :increment_counter_caches after_destroy_commit :decrement_counter_caches diff --git a/app/models/tag_filter.rb b/app/models/tag_filter.rb deleted file mode 100644 index ecdb52503..000000000 --- a/app/models/tag_filter.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -class TagFilter - KEYS = %i( - trending - status - ).freeze - - attr_reader :params - - def initialize(params) - @params = params - end - - def results - scope = begin - if params[:status] == 'pending_review' - Tag.unscoped - else - trending_scope - end - end - - params.each do |key, value| - next if key.to_s == 'page' - - scope.merge!(scope_for(key, value.to_s.strip)) if value.present? - end - - scope - end - - private - - def scope_for(key, value) - case key.to_s - when 'status' - status_scope(value) - else - raise "Unknown filter: #{key}" - end - end - - def trending_scope - ids = Trends.tags.currently_trending_ids(false, -1) - - if ids.empty? - Tag.none - else - Tag.joins("join unnest(array[#{ids.map(&:to_i).join(',')}]::integer[]) with ordinality as x (id, ordering) on tags.id = x.id").order('x.ordering') - end - end - - def status_scope(value) - case value.to_s - when 'approved' - Tag.trendable - when 'rejected' - Tag.not_trendable - when 'pending_review' - Tag.pending_review - else - raise "Unknown status: #{value}" - end - end -end diff --git a/app/models/trends.rb b/app/models/trends.rb index 7dd3a9c87..f8864e55f 100644 --- a/app/models/trends.rb +++ b/app/models/trends.rb @@ -13,15 +13,37 @@ module Trends @tags ||= Trends::Tags.new end + def self.statuses + @statuses ||= Trends::Statuses.new + end + + def self.register!(status) + [links, tags, statuses].each { |trend_type| trend_type.register(status) } + end + def self.refresh! - [links, tags].each(&:refresh) + [links, tags, statuses].each(&:refresh) end def self.request_review! - [links, tags].each(&:request_review) if enabled? + return unless enabled? + + links_requiring_review = links.request_review + tags_requiring_review = tags.request_review + statuses_requiring_review = statuses.request_review + + return if links_requiring_review.empty? && tags_requiring_review.empty? && statuses_requiring_review.empty? + + User.staff.includes(:account).find_each do |user| + AdminMailer.new_trends(user.account, links_requiring_review, tags_requiring_review, statuses_requiring_review).deliver_later! if user.allows_trends_review_emails? + end end def self.enabled? Setting.trends end + + def self.available_locales + @available_locales ||= I18n.available_locales.map { |locale| locale.to_s.split(/[_-]/).first }.uniq + end end diff --git a/app/models/trends/base.rb b/app/models/trends/base.rb index b767dcb1a..7ed13228d 100644 --- a/app/models/trends/base.rb +++ b/app/models/trends/base.rb @@ -2,6 +2,7 @@ class Trends::Base include Redisable + include LanguagesHelper class_attribute :default_options @@ -32,8 +33,8 @@ class Trends::Base raise NotImplementedError end - def get(*) - raise NotImplementedError + def query + Trends::Query.new(key_prefix, klass) end def score(id) @@ -72,6 +73,21 @@ class Trends::Base redis.zrevrange("#{key_prefix}:allowed", 0, rank, with_scores: true).last&.last || 0 end + # @param [Integer] id + # @param [Float] score + # @param [Hash] subsets + def add_to_and_remove_from_subsets(id, score, subsets = {}) + subsets.each_key do |subset| + key = [key_prefix, subset].compact.join(':') + + if score.positive? && subsets[subset] + redis.zadd(key, score, id) + else + redis.zrem(key, id) + end + end + end + private def used_key(at_time) diff --git a/app/models/trends/links.rb b/app/models/trends/links.rb index a0d65138b..62308e706 100644 --- a/app/models/trends/links.rb +++ b/app/models/trends/links.rb @@ -4,8 +4,8 @@ class Trends::Links < Trends::Base PREFIX = 'trending_links' self.default_options = { - threshold: 15, - review_threshold: 10, + threshold: 5, + review_threshold: 3, max_score_cooldown: 2.days.freeze, max_score_halflife: 8.hours.freeze, } @@ -27,12 +27,6 @@ class Trends::Links < Trends::Base record_used_id(preview_card.id, at_time) end - def get(allowed, limit) - preview_card_ids = currently_trending_ids(allowed, limit) - preview_cards = PreviewCard.where(id: preview_card_ids).index_by(&:id) - preview_card_ids.map { |id| preview_cards[id] }.compact - end - def refresh(at_time = Time.now.utc) preview_cards = PreviewCard.where(id: (recently_used_ids(at_time) + currently_trending_ids(false, -1)).uniq) calculate_scores(preview_cards, at_time) @@ -42,7 +36,7 @@ class Trends::Links < Trends::Base def request_review preview_cards = PreviewCard.where(id: currently_trending_ids(false, -1)) - preview_cards_requiring_review = preview_cards.filter_map do |preview_card| + preview_cards.filter_map do |preview_card| next unless would_be_trending?(preview_card.id) && !preview_card.trendable? && preview_card.requires_review_notification? if preview_card.provider.nil? @@ -53,12 +47,6 @@ class Trends::Links < Trends::Base preview_card end - - return if preview_cards_requiring_review.empty? - - User.staff.includes(:account).find_each do |user| - AdminMailer.new_trending_links(user.account, preview_cards_requiring_review).deliver_later! if user.allows_trending_tag_emails? - end end protected @@ -67,6 +55,10 @@ class Trends::Links < Trends::Base PREFIX end + def klass + PreviewCard + end + private def calculate_scores(preview_cards, at_time) @@ -96,17 +88,27 @@ class Trends::Links < Trends::Base decaying_score = max_score * (0.5**((at_time.to_f - max_time.to_f) / options[:max_score_halflife].to_f)) - if decaying_score.zero? - redis.zrem("#{PREFIX}:all", preview_card.id) - redis.zrem("#{PREFIX}:allowed", preview_card.id) - else - redis.zadd("#{PREFIX}:all", decaying_score, preview_card.id) + add_to_and_remove_from_subsets(preview_card.id, decaying_score, { + all: true, + allowed: preview_card.trendable?, + }) - if preview_card.trendable? - redis.zadd("#{PREFIX}:allowed", decaying_score, preview_card.id) - else - redis.zrem("#{PREFIX}:allowed", preview_card.id) - end + next unless valid_locale?(preview_card.language) + + add_to_and_remove_from_subsets(preview_card.id, decaying_score, { + "all:#{preview_card.language}" => true, + "allowed:#{preview_card.language}" => preview_card.trendable?, + }) + end + + # Clean up localized sets by calculating the intersection with the main + # set. We do this instead of just deleting the localized sets to avoid + # having moments where the API returns empty results + + redis.pipelined do + Trends.available_locales.each do |locale| + redis.zinterstore("#{key_prefix}:all:#{locale}", ["#{key_prefix}:all:#{locale}", "#{key_prefix}:all"], aggregate: 'max') + redis.zinterstore("#{key_prefix}:allowed:#{locale}", ["#{key_prefix}:allowed:#{locale}", "#{key_prefix}:all"], aggregate: 'max') end end end diff --git a/app/models/trends/preview_card_batch.rb b/app/models/trends/preview_card_batch.rb new file mode 100644 index 000000000..b1d682910 --- /dev/null +++ b/app/models/trends/preview_card_batch.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class Trends::PreviewCardBatch + include ActiveModel::Model + include Authorization + + attr_accessor :preview_card_ids, :action, :current_account, :precision + + def save + case action + when 'approve' + approve! + when 'approve_providers' + approve_providers! + when 'reject' + reject! + when 'reject_providers' + reject_providers! + end + end + + private + + def preview_cards + @preview_cards ||= PreviewCard.where(id: preview_card_ids) + end + + def preview_card_providers + @preview_card_providers ||= preview_cards.map(&:domain).uniq.map { |domain| PreviewCardProvider.matching_domain(domain) || PreviewCardProvider.new(domain: domain) } + end + + def approve! + preview_cards.each { |preview_card| authorize(preview_card, :review?) } + preview_cards.update_all(trendable: true) + end + + def approve_providers! + preview_card_providers.each do |provider| + authorize(provider, :review?) + provider.update(trendable: true, reviewed_at: action_time) + end + + # Reset any individual overrides + preview_cards.update_all(trendable: nil) + end + + def reject! + preview_cards.each { |preview_card| authorize(preview_card, :review?) } + preview_cards.update_all(trendable: false) + end + + def reject_providers! + preview_card_providers.each do |provider| + authorize(provider, :review?) + provider.update(trendable: false, reviewed_at: action_time) + end + + # Reset any individual overrides + preview_cards.update_all(trendable: nil) + end + + def action_time + @action_time ||= Time.now.utc + end +end diff --git a/app/models/trends/preview_card_filter.rb b/app/models/trends/preview_card_filter.rb new file mode 100644 index 000000000..25add58c8 --- /dev/null +++ b/app/models/trends/preview_card_filter.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class Trends::PreviewCardFilter + KEYS = %i( + trending + locale + ).freeze + + attr_reader :params + + def initialize(params) + @params = params + end + + def results + scope = PreviewCard.unscoped + + params.each do |key, value| + next if %w(page locale).include?(key.to_s) + + scope.merge!(scope_for(key, value.to_s.strip)) if value.present? + end + + scope + end + + private + + def scope_for(key, value) + case key.to_s + when 'trending' + trending_scope(value) + else + raise "Unknown filter: #{key}" + end + end + + def trending_scope(value) + scope = Trends.links.query + + scope = scope.in_locale(@params[:locale].to_s) if @params[:locale].present? + scope = scope.allowed if value == 'allowed' + + scope.to_arel + end +end diff --git a/app/models/trends/preview_card_provider_batch.rb b/app/models/trends/preview_card_provider_batch.rb new file mode 100644 index 000000000..062720c81 --- /dev/null +++ b/app/models/trends/preview_card_provider_batch.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class Trends::PreviewCardProviderBatch + include ActiveModel::Model + include Authorization + + attr_accessor :preview_card_provider_ids, :action, :current_account + + def save + case action + when 'approve' + approve! + when 'reject' + reject! + end + end + + private + + def preview_card_providers + PreviewCardProvider.where(id: preview_card_provider_ids) + end + + def approve! + preview_card_providers.each { |provider| authorize(provider, :review?) } + preview_card_providers.update_all(trendable: true, reviewed_at: Time.now.utc) + end + + def reject! + preview_card_providers.each { |provider| authorize(provider, :review?) } + preview_card_providers.update_all(trendable: false, reviewed_at: Time.now.utc) + end +end diff --git a/app/models/trends/preview_card_provider_filter.rb b/app/models/trends/preview_card_provider_filter.rb new file mode 100644 index 000000000..abfdd07e8 --- /dev/null +++ b/app/models/trends/preview_card_provider_filter.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +class Trends::PreviewCardProviderFilter + KEYS = %i( + status + ).freeze + + attr_reader :params + + def initialize(params) + @params = params + end + + def results + scope = PreviewCardProvider.unscoped + + params.each do |key, value| + next if key.to_s == 'page' + + scope.merge!(scope_for(key, value.to_s.strip)) if value.present? + end + + scope.order(domain: :asc) + end + + private + + def scope_for(key, value) + case key.to_s + when 'status' + status_scope(value) + else + raise "Unknown filter: #{key}" + end + end + + def status_scope(value) + case value.to_s + when 'approved' + PreviewCardProvider.trendable + when 'rejected' + PreviewCardProvider.not_trendable + when 'pending_review' + PreviewCardProvider.pending_review + else + raise "Unknown status: #{value}" + end + end +end diff --git a/app/models/trends/query.rb b/app/models/trends/query.rb new file mode 100644 index 000000000..64a4c0c1f --- /dev/null +++ b/app/models/trends/query.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +class Trends::Query + include Redisable + include Enumerable + + attr_reader :prefix, :klass, :loaded + + alias loaded? loaded + + def initialize(prefix, klass) + @prefix = prefix + @klass = klass + @records = [] + @loaded = false + @allowed = false + @limit = -1 + @offset = 0 + end + + def allowed! + @allowed = true + self + end + + def allowed + clone.allowed! + end + + def in_locale!(value) + @locale = value + self + end + + def in_locale(value) + clone.in_locale!(value) + end + + def offset!(value) + @offset = value + self + end + + def offset(value) + clone.offset!(value) + end + + def limit!(value) + @limit = value + self + end + + def limit(value) + clone.limit!(value) + end + + def records + load + @records + end + + delegate :each, :empty?, :first, :last, to: :records + + def to_ary + records.dup + end + + alias to_a to_ary + + def to_arel + tmp_ids = ids + + if tmp_ids.empty? + klass.none + else + klass.joins("join unnest(array[#{tmp_ids.join(',')}]) with ordinality as x (id, ordering) on #{klass.table_name}.id = x.id").reorder('x.ordering') + end + end + + private + + def key + [@prefix, @allowed ? 'allowed' : 'all', @locale].compact.join(':') + end + + def load + unless loaded? + @records = perform_queries + @loaded = true + end + + self + end + + def ids + redis.zrevrange(key, @offset, @limit.positive? ? @limit - 1 : @limit).map(&:to_i) + end + + def perform_queries + apply_scopes(to_arel).to_a + end + + def apply_scopes(scope) + scope + end +end diff --git a/app/models/trends/status_batch.rb b/app/models/trends/status_batch.rb new file mode 100644 index 000000000..78d93bed4 --- /dev/null +++ b/app/models/trends/status_batch.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class Trends::StatusBatch + include ActiveModel::Model + include Authorization + + attr_accessor :status_ids, :action, :current_account + + def save + case action + when 'approve' + approve! + when 'approve_accounts' + approve_accounts! + when 'reject' + reject! + when 'reject_accounts' + reject_accounts! + end + end + + private + + def statuses + @statuses ||= Status.where(id: status_ids) + end + + def status_accounts + @status_accounts ||= Account.where(id: statuses.map(&:account_id).uniq) + end + + def approve! + statuses.each { |status| authorize(status, :review?) } + statuses.update_all(trendable: true) + end + + def approve_accounts! + status_accounts.each do |account| + authorize(account, :review?) + account.update(trendable: true, reviewed_at: action_time) + end + + # Reset any individual overrides + statuses.update_all(trendable: nil) + end + + def reject! + statuses.each { |status| authorize(status, :review?) } + statuses.update_all(trendable: false) + end + + def reject_accounts! + status_accounts.each do |account| + authorize(account, :review?) + account.update(trendable: false, reviewed_at: action_time) + end + + # Reset any individual overrides + statuses.update_all(trendable: nil) + end + + def action_time + @action_time ||= Time.now.utc + end +end diff --git a/app/models/trends/status_filter.rb b/app/models/trends/status_filter.rb new file mode 100644 index 000000000..7c453e339 --- /dev/null +++ b/app/models/trends/status_filter.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class Trends::StatusFilter + KEYS = %i( + trending + locale + ).freeze + + attr_reader :params + + def initialize(params) + @params = params + end + + def results + scope = Status.unscoped.kept + + params.each do |key, value| + next if %w(page locale).include?(key.to_s) + + scope.merge!(scope_for(key, value.to_s.strip)) if value.present? + end + + scope + end + + private + + def scope_for(key, value) + case key.to_s + when 'trending' + trending_scope(value) + else + raise "Unknown filter: #{key}" + end + end + + def trending_scope(value) + scope = Trends.statuses.query + + scope = scope.in_locale(@params[:locale].to_s) if @params[:locale].present? + scope = scope.allowed if value == 'allowed' + + scope.to_arel + end +end diff --git a/app/models/trends/statuses.rb b/app/models/trends/statuses.rb new file mode 100644 index 000000000..e785413ec --- /dev/null +++ b/app/models/trends/statuses.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +class Trends::Statuses < Trends::Base + PREFIX = 'trending_statuses' + + self.default_options = { + threshold: 5, + review_threshold: 3, + score_halflife: 2.hours.freeze, + } + + class Query < Trends::Query + def filtered_for!(account) + @account = account + self + end + + def filtered_for(account) + clone.filtered_for!(account) + end + + private + + def apply_scopes(scope) + scope.includes(:account) + end + + def perform_queries + return super if @account.nil? + + statuses = super + account_ids = statuses.map(&:account_id) + account_domains = statuses.map(&:account_domain) + + preloaded_relations = { + blocking: Account.blocking_map(account_ids, @account.id), + blocked_by: Account.blocked_by_map(account_ids, @account.id), + muting: Account.muting_map(account_ids, @account.id), + following: Account.following_map(account_ids, @account.id), + domain_blocking_by_domain: Account.domain_blocking_map_by_domain(account_domains, @account.id), + } + + statuses.reject { |status| StatusFilter.new(status, @account, preloaded_relations).filtered? } + end + end + + def register(status, at_time = Time.now.utc) + add(status.proper, status.account_id, at_time) if eligible?(status) + end + + def add(status, _account_id, at_time = Time.now.utc) + # We rely on the total reblogs and favourites count, so we + # don't record which account did the what and when here + + record_used_id(status.id, at_time) + end + + def query + Query.new(key_prefix, klass) + end + + def refresh(at_time = Time.now.utc) + statuses = Status.where(id: (recently_used_ids(at_time) + currently_trending_ids(false, -1)).uniq).includes(:account, :media_attachments) + calculate_scores(statuses, at_time) + trim_older_items + end + + def request_review + statuses = Status.where(id: currently_trending_ids(false, -1)).includes(:account) + + statuses.filter_map do |status| + next unless would_be_trending?(status.id) && !status.trendable? && status.requires_review_notification? + + status.account.touch(:requested_review_at) + status + end + end + + protected + + def key_prefix + PREFIX + end + + def klass + Status + end + + private + + def eligible?(status) + original_status = status.proper + + original_status.public_visibility? && + original_status.account.discoverable? && !original_status.account.silenced? && + original_status.spoiler_text.blank? && !original_status.sensitive? && !original_status.reply? + end + + def calculate_scores(statuses, at_time) + redis.pipelined do + statuses.each do |status| + expected = 1.0 + observed = (status.reblogs_count + status.favourites_count).to_f + + score = begin + if expected > observed || observed < options[:threshold] + 0 + else + ((observed - expected)**2) / expected + end + end + + decaying_score = score * (0.5**((at_time.to_f - status.created_at.to_f) / options[:score_halflife].to_f)) + + add_to_and_remove_from_subsets(status.id, decaying_score, { + all: true, + allowed: status.trendable? && status.account.discoverable?, + }) + + next unless valid_locale?(status.language) + + add_to_and_remove_from_subsets(status.id, decaying_score, { + "all:#{status.language}" => true, + "allowed:#{status.language}" => status.trendable? && status.account.discoverable?, + }) + end + + # Clean up localized sets by calculating the intersection with the main + # set. We do this instead of just deleting the localized sets to avoid + # having moments where the API returns empty results + + Trends.available_locales.each do |locale| + redis.zinterstore("#{key_prefix}:all:#{locale}", ["#{key_prefix}:all:#{locale}", "#{key_prefix}:all"], aggregate: 'max') + redis.zinterstore("#{key_prefix}:allowed:#{locale}", ["#{key_prefix}:allowed:#{locale}", "#{key_prefix}:all"], aggregate: 'max') + end + end + end + + def would_be_trending?(id) + score(id) > score_at_rank(options[:review_threshold] - 1) + end +end diff --git a/app/models/trends/tag_batch.rb b/app/models/trends/tag_batch.rb new file mode 100644 index 000000000..16ee08c06 --- /dev/null +++ b/app/models/trends/tag_batch.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class Trends::TagBatch + include ActiveModel::Model + include Authorization + + attr_accessor :tag_ids, :action, :current_account + + def save + case action + when 'approve' + approve! + when 'reject' + reject! + end + end + + private + + def tags + Tag.where(id: tag_ids) + end + + def approve! + tags.each { |tag| authorize(tag, :review?) } + tags.update_all(trendable: true, reviewed_at: action_time) + end + + def reject! + tags.each { |tag| authorize(tag, :review?) } + tags.update_all(trendable: false, reviewed_at: action_time) + end + + def action_time + @action_time ||= Time.now.utc + end +end diff --git a/app/models/trends/tag_filter.rb b/app/models/trends/tag_filter.rb new file mode 100644 index 000000000..3b142efc4 --- /dev/null +++ b/app/models/trends/tag_filter.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +class Trends::TagFilter + KEYS = %i( + trending + status + ).freeze + + attr_reader :params + + def initialize(params) + @params = params + end + + def results + scope = begin + if params[:status] == 'pending_review' + Tag.unscoped + else + trending_scope + end + end + + params.each do |key, value| + next if key.to_s == 'page' + + scope.merge!(scope_for(key, value.to_s.strip)) if value.present? + end + + scope + end + + private + + def scope_for(key, value) + case key.to_s + when 'status' + status_scope(value) + else + raise "Unknown filter: #{key}" + end + end + + def trending_scope + Trends.tags.query.to_arel + end + + def status_scope(value) + case value.to_s + when 'approved' + Tag.trendable + when 'rejected' + Tag.not_trendable + when 'pending_review' + Tag.pending_review + else + raise "Unknown status: #{value}" + end + end +end diff --git a/app/models/trends/tags.rb b/app/models/trends/tags.rb index 2ea4550df..3caa58815 100644 --- a/app/models/trends/tags.rb +++ b/app/models/trends/tags.rb @@ -5,7 +5,7 @@ class Trends::Tags < Trends::Base self.default_options = { threshold: 5, - review_threshold: 10, + review_threshold: 3, max_score_cooldown: 2.days.freeze, max_score_halflife: 4.hours.freeze, } @@ -29,27 +29,15 @@ class Trends::Tags < Trends::Base trim_older_items end - def get(allowed, limit) - tag_ids = currently_trending_ids(allowed, limit) - tags = Tag.where(id: tag_ids).index_by(&:id) - tag_ids.map { |id| tags[id] }.compact - end - def request_review tags = Tag.where(id: currently_trending_ids(false, -1)) - tags_requiring_review = tags.filter_map do |tag| + tags.filter_map do |tag| next unless would_be_trending?(tag.id) && !tag.trendable? && tag.requires_review_notification? tag.touch(:requested_review_at) tag end - - return if tags_requiring_review.empty? - - User.staff.includes(:account).find_each do |user| - AdminMailer.new_trending_tags(user.account, tags_requiring_review).deliver_later! if user.allows_trending_tag_emails? - end end protected @@ -58,6 +46,10 @@ class Trends::Tags < Trends::Base PREFIX end + def klass + Tag + end + private def calculate_scores(tags, at_time) @@ -87,18 +79,10 @@ class Trends::Tags < Trends::Base decaying_score = max_score * (0.5**((at_time.to_f - max_time.to_f) / options[:max_score_halflife].to_f)) - if decaying_score.zero? - redis.zrem("#{PREFIX}:all", tag.id) - redis.zrem("#{PREFIX}:allowed", tag.id) - else - redis.zadd("#{PREFIX}:all", decaying_score, tag.id) - - if tag.trendable? - redis.zadd("#{PREFIX}:allowed", decaying_score, tag.id) - else - redis.zrem("#{PREFIX}:allowed", tag.id) - end - end + add_to_and_remove_from_subsets(tag.id, decaying_score, { + all: true, + allowed: tag.trendable?, + }) end end diff --git a/app/models/user.rb b/app/models/user.rb index 517254a91..bbf850d84 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -269,7 +269,7 @@ class User < ApplicationRecord settings.notification_emails['appeal'] end - def allows_trending_tag_emails? + def allows_trends_review_emails? settings.notification_emails['trending_tag'] end diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb index 46237e45c..cc23771e7 100644 --- a/app/policies/account_policy.rb +++ b/app/policies/account_policy.rb @@ -68,4 +68,8 @@ class AccountPolicy < ApplicationPolicy def unblock_email? staff? end + + def review? + staff? + end end diff --git a/app/policies/preview_card_policy.rb b/app/policies/preview_card_policy.rb index 4f485d7fc..0410987e4 100644 --- a/app/policies/preview_card_policy.rb +++ b/app/policies/preview_card_policy.rb @@ -5,7 +5,7 @@ class PreviewCardPolicy < ApplicationPolicy staff? end - def update? + def review? staff? end end diff --git a/app/policies/preview_card_provider_policy.rb b/app/policies/preview_card_provider_policy.rb index 598d54a5e..44d2ad5cf 100644 --- a/app/policies/preview_card_provider_policy.rb +++ b/app/policies/preview_card_provider_policy.rb @@ -5,7 +5,7 @@ class PreviewCardProviderPolicy < ApplicationPolicy staff? end - def update? + def review? staff? end end diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 6e9b840db..400f1ec79 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -41,6 +41,10 @@ class StatusPolicy < ApplicationPolicy staff? || owned? end + def review? + staff? + end + private def requires_mention? diff --git a/app/policies/tag_policy.rb b/app/policies/tag_policy.rb index aaf70fcab..bdfcec0c9 100644 --- a/app/policies/tag_policy.rb +++ b/app/policies/tag_policy.rb @@ -12,4 +12,8 @@ class TagPolicy < ApplicationPolicy def update? staff? end + + def review? + staff? + end end diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb index a572a7c59..a2d535d26 100644 --- a/app/services/delete_account_service.rb +++ b/app/services/delete_account_service.rb @@ -220,21 +220,23 @@ class DeleteAccountService < BaseService return unless keep_account_record? - @account.silenced_at = nil - @account.suspended_at = @options[:suspended_at] || Time.now.utc - @account.suspension_origin = :local - @account.locked = false - @account.memorial = false - @account.discoverable = false - @account.display_name = '' - @account.note = '' - @account.fields = [] - @account.statuses_count = 0 - @account.followers_count = 0 - @account.following_count = 0 - @account.moved_to_account = nil - @account.also_known_as = [] - @account.trust_level = :untrusted + @account.silenced_at = nil + @account.suspended_at = @options[:suspended_at] || Time.now.utc + @account.suspension_origin = :local + @account.locked = false + @account.memorial = false + @account.discoverable = false + @account.trendable = false + @account.display_name = '' + @account.note = '' + @account.fields = [] + @account.statuses_count = 0 + @account.followers_count = 0 + @account.following_count = 0 + @account.moved_to_account = nil + @account.reviewed_at = nil + @account.requested_review_at = nil + @account.also_known_as = [] @account.avatar.destroy @account.header.destroy @account.save! diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index a0ab3b4b7..0ca0081b4 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -17,6 +17,8 @@ class FavouriteService < BaseService favourite = Favourite.create!(account: account, status: status) + Trends.statuses.register(status) + create_notification(favourite) bump_potential_friendship(account, status) diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 2d1265f10..7d2981709 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -30,8 +30,7 @@ class ReblogService < BaseService reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility, rate_limit: options[:with_rate_limit]) - Trends.tags.register(reblog) - Trends.links.register(reblog) + Trends.register!(reblog) DistributionWorker.perform_async(reblog.id) ActivityPub::DistributionWorker.perform_async(reblog.id) diff --git a/app/views/admin/custom_emojis/_custom_emoji.html.haml b/app/views/admin/custom_emojis/_custom_emoji.html.haml index 526c844e9..41f3975cf 100644 --- a/app/views/admin/custom_emojis/_custom_emoji.html.haml +++ b/app/views/admin/custom_emojis/_custom_emoji.html.haml @@ -3,7 +3,7 @@ = f.check_box :custom_emoji_ids, { multiple: true, include_hidden: false }, custom_emoji.id .batch-table__row__content.batch-table__row__content--with-image .batch-table__row__content__image - = custom_emoji_tag(custom_emoji, animate = current_account&.user&.setting_auto_play_gif) + = custom_emoji_tag(custom_emoji, current_account&.user&.setting_auto_play_gif) .batch-table__row__content__text %samp= ":#{custom_emoji.shortcode}:" diff --git a/app/views/admin/follow_recommendations/show.html.haml b/app/views/admin/follow_recommendations/show.html.haml index 272681864..ebc4a2c6b 100644 --- a/app/views/admin/follow_recommendations/show.html.haml +++ b/app/views/admin/follow_recommendations/show.html.haml @@ -9,12 +9,14 @@ %hr.spacer/ = form_tag admin_follow_recommendations_path, method: 'GET', class: 'simple_form' do + - RelationshipFilter::KEYS.each do |key| + = hidden_field_tag key, params[key] if params[key].present? + .filters .filter-subset.filter-subset--with-select %strong= t('admin.follow_recommendations.language') .input.select.optional - = select_tag :language, options_for_select(I18n.available_locales.map { |key| key.to_s.split(/[_-]/).first.to_sym }.uniq.map { |key| [standard_locale_name(key), key]}, @language) - + = select_tag :language, options_for_select(Trends.available_locales.map { |key| [standard_locale_name(key), key] }, @language) .filter-subset %strong= t('admin.follow_recommendations.status') %ul diff --git a/app/views/admin/trends/links/index.html.haml b/app/views/admin/trends/links/index.html.haml index 240ae722b..79f3513d3 100644 --- a/app/views/admin/trends/links/index.html.haml +++ b/app/views/admin/trends/links/index.html.haml @@ -4,23 +4,29 @@ - content_for :header_tags do = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' -.filters - .filter-subset - %strong= t('admin.trends.trending') - %ul - %li= filter_link_to t('generic.all'), trending: nil - %li= filter_link_to t('admin.trends.only_allowed'), trending: 'allowed' - .back-link - = link_to admin_trends_links_preview_card_providers_path do - = t('admin.trends.preview_card_providers.title') - = fa_icon 'chevron-right fw' += form_tag admin_trends_links_path, method: 'GET', class: 'simple_form' do + - Trends::PreviewCardFilter::KEYS.each do |key| + = hidden_field_tag key, params[key] if params[key].present? -%hr.spacer/ + .filters + .filter-subset.filter-subset--with-select + %strong= t('admin.follow_recommendations.language') + .input.select.optional + = select_tag :locale, options_for_select(Trends.available_locales.map { |key| [standard_locale_name(key), key] }, params[:locale]), include_blank: true + .filter-subset + %strong= t('admin.trends.trending') + %ul + %li= filter_link_to t('generic.all'), trending: nil + %li= filter_link_to t('admin.trends.only_allowed'), trending: 'allowed' + .back-link + = link_to admin_trends_links_preview_card_providers_path do + = t('admin.trends.preview_card_providers.title') + = fa_icon 'chevron-right fw' = form_for(@form, url: batch_admin_trends_links_path) do |f| = hidden_field_tag :page, params[:page] || 1 - - PreviewCardFilter::KEYS.each do |key| + - Trends::PreviewCardFilter::KEYS.each do |key| = hidden_field_tag key, params[key] if params[key].present? .batch-table @@ -29,9 +35,9 @@ = check_box_tag :batch_checkbox_all, nil, false .batch-table__toolbar__actions = f.button safe_join([fa_icon('check'), t('admin.trends.links.allow')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } - = f.button safe_join([fa_icon('check'), t('admin.trends.links.allow_provider')]), name: :approve_all, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + = f.button safe_join([fa_icon('check'), t('admin.trends.links.allow_provider')]), name: :approve_providers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } = f.button safe_join([fa_icon('times'), t('admin.trends.links.disallow')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } - = f.button safe_join([fa_icon('times'), t('admin.trends.links.disallow_provider')]), name: :reject_all, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + = f.button safe_join([fa_icon('times'), t('admin.trends.links.disallow_provider')]), name: :reject_providers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } .batch-table__body - if @preview_cards.empty? = nothing_here 'nothing-here--under-tabs' diff --git a/app/views/admin/trends/links/preview_card_providers/index.html.haml b/app/views/admin/trends/links/preview_card_providers/index.html.haml index eac6e641f..b79349947 100644 --- a/app/views/admin/trends/links/preview_card_providers/index.html.haml +++ b/app/views/admin/trends/links/preview_card_providers/index.html.haml @@ -23,7 +23,7 @@ = form_for(@form, url: batch_admin_trends_links_preview_card_providers_path) do |f| = hidden_field_tag :page, params[:page] || 1 - - PreviewCardProviderFilter::KEYS.each do |key| + - Trends::PreviewCardProviderFilter::KEYS.each do |key| = hidden_field_tag key, params[key] if params[key].present? .batch-table.optional diff --git a/app/views/admin/trends/statuses/_status.html.haml b/app/views/admin/trends/statuses/_status.html.haml new file mode 100644 index 000000000..c99ee5d60 --- /dev/null +++ b/app/views/admin/trends/statuses/_status.html.haml @@ -0,0 +1,30 @@ +.batch-table__row{ class: [status.account.requires_review? && 'batch-table__row--attention', !status.account.requires_review? && !status.trendable? && 'batch-table__row--muted'] } + %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox + = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id + + .batch-table__row__content.pending-account__header + .one-liner + = admin_account_link_to status.account + + = link_to ActivityPub::TagManager.instance.url_for(status), target: '_blank', class: 'emojify', rel: 'noopener noreferrer' do + = one_line_preview(status) + + - status.media_attachments.each do |media_attachment| + %abbr{ title: media_attachment.description } + = fa_icon 'link' + = media_attachment.file_file_name + + = t('admin.trends.statuses.shared_by', count: status.reblogs_count + status.favourites_count, friendly_count: friendly_number_to_human(status.reblogs_count + status.favourites_count)) + + - if status.account.domain.present? + • + = status.account.domain + - if status.language.present? + • + = standard_locale_name(status.language) + - if status.trendable? && (rank = Trends.statuses.rank(status.id)) + • + %abbr{ title: t('admin.trends.tags.current_score', score: Trends.statuses.score(status.id)) }= t('admin.trends.tags.trending_rank', rank: rank + 1) + - elsif status.account.requires_review? + • + = t('admin.trends.pending_review') diff --git a/app/views/admin/trends/statuses/index.html.haml b/app/views/admin/trends/statuses/index.html.haml new file mode 100644 index 000000000..347688262 --- /dev/null +++ b/app/views/admin/trends/statuses/index.html.haml @@ -0,0 +1,43 @@ +- content_for :page_title do + = t('admin.trends.statuses.title') + +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + += form_tag admin_trends_statuses_path, method: 'GET', class: 'simple_form' do + - Trends::StatusFilter::KEYS.each do |key| + = hidden_field_tag key, params[key] if params[key].present? + + .filters + .filter-subset.filter-subset--with-select + %strong= t('admin.follow_recommendations.language') + .input.select.optional + = select_tag :locale, options_for_select(Trends.available_locales.map { |key| [standard_locale_name(key), key]}, params[:locale]), include_blank: true + .filter-subset + %strong= t('admin.trends.trending') + %ul + %li= filter_link_to t('generic.all'), trending: nil + %li= filter_link_to t('admin.trends.only_allowed'), trending: 'allowed' + += form_for(@form, url: batch_admin_trends_statuses_path) do |f| + = hidden_field_tag :page, params[:page] || 1 + + - Trends::StatusFilter::KEYS.each do |key| + = hidden_field_tag key, params[key] if params[key].present? + + .batch-table + .batch-table__toolbar + %label.batch-table__toolbar__select.batch-checkbox-all + = check_box_tag :batch_checkbox_all, nil, false + .batch-table__toolbar__actions + = f.button safe_join([fa_icon('check'), t('admin.trends.statuses.allow')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + = f.button safe_join([fa_icon('check'), t('admin.trends.statuses.allow_account')]), name: :approve_accounts, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + = f.button safe_join([fa_icon('times'), t('admin.trends.statuses.disallow')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + = f.button safe_join([fa_icon('times'), t('admin.trends.statuses.disallow_account')]), name: :reject_accounts, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + .batch-table__body + - if @statuses.empty? + = nothing_here 'nothing-here--under-tabs' + - else + = render partial: 'status', collection: @statuses, locals: { f: f } + += paginate @statuses diff --git a/app/views/admin/trends/tags/index.html.haml b/app/views/admin/trends/tags/index.html.haml index 8df0a9920..8a2f785bc 100644 --- a/app/views/admin/trends/tags/index.html.haml +++ b/app/views/admin/trends/tags/index.html.haml @@ -13,12 +13,10 @@ %li= filter_link_to t('admin.trends.rejected'), status: 'rejected' %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{Tag.pending_review.count})"], ' '), status: 'pending_review' -%hr.spacer/ - = form_for(@form, url: batch_admin_trends_tags_path) do |f| = hidden_field_tag :page, params[:page] || 1 - - TagFilter::KEYS.each do |key| + - Trends::TagFilter::KEYS.each do |key| = hidden_field_tag key, params[key] if params[key].present? .batch-table.optional diff --git a/app/views/admin_mailer/_new_trending_links.text.erb b/app/views/admin_mailer/_new_trending_links.text.erb new file mode 100644 index 000000000..405926fdd --- /dev/null +++ b/app/views/admin_mailer/_new_trending_links.text.erb @@ -0,0 +1,14 @@ +<%= raw t('admin_mailer.new_trends.new_trending_links.title') %> + +<% @links.each do |link| %> +- <%= link.title %> • <%= link.url %> + <%= raw t('admin.trends.links.usage_comparison', today: link.history.get(Time.now.utc).accounts, yesterday: link.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: Trends.links.score(link.id).round(2)) %> +<% end %> + +<% if @lowest_trending_link %> +<%= raw t('admin_mailer.new_trends.new_trending_links.requirements', lowest_link_title: @lowest_trending_link.title, lowest_link_score: Trends.links.score(@lowest_trending_link.id).round(2), rank: Trends.links.options[:review_threshold]) %> +<% else %> +<%= raw t('admin_mailer.new_trends.new_trending_links.no_approved_links') %> +<% end %> + +<%= raw t('application_mailer.view')%> <%= admin_trends_links_url %> diff --git a/app/views/admin_mailer/_new_trending_statuses.text.erb b/app/views/admin_mailer/_new_trending_statuses.text.erb new file mode 100644 index 000000000..8d11a80c2 --- /dev/null +++ b/app/views/admin_mailer/_new_trending_statuses.text.erb @@ -0,0 +1,14 @@ +<%= raw t('admin_mailer.new_trends.new_trending_statuses.title') %> + +<% @statuses.each do |status| %> +- <%= ActivityPub::TagManager.instance.url_for(status) %> + <%= raw t('admin.trends.tags.current_score', score: Trends.statuses.score(status.id).round(2)) %> +<% end %> + +<% if @lowest_trending_status %> +<%= raw t('admin_mailer.new_trends.new_trending_statuses.requirements', lowest_status_url: ActivityPub::TagManager.instance.url_for(@lowest_trending_status), lowest_status_score: Trends.statuses.score(@lowest_trending_status.id).round(2), rank: Trends.statuses.options[:review_threshold]) %> +<% else %> +<%= raw t('admin_mailer.new_trends.new_trending_statuses.no_approved_statuses') %> +<% end %> + +<%= raw t('application_mailer.view')%> <%= admin_trends_statuses_url %> diff --git a/app/views/admin_mailer/_new_trending_tags.text.erb b/app/views/admin_mailer/_new_trending_tags.text.erb new file mode 100644 index 000000000..49fe84309 --- /dev/null +++ b/app/views/admin_mailer/_new_trending_tags.text.erb @@ -0,0 +1,14 @@ +<%= raw t('admin_mailer.new_trends.new_trending_tags.title') %> + +<% @tags.each do |tag| %> +- #<%= tag.name %> + <%= raw t('admin.trends.tags.usage_comparison', today: tag.history.get(Time.now.utc).accounts, yesterday: tag.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: Trends.tags.score(tag.id).round(2)) %> +<% end %> + +<% if @lowest_trending_tag %> +<%= raw t('admin_mailer.new_trends.new_trending_tags.requirements', lowest_tag_name: @lowest_trending_tag.name, lowest_tag_score: Trends.tags.score(@lowest_trending_tag.id).round(2), rank: Trends.tags.options[:review_threshold]) %> +<% else %> +<%= raw t('admin_mailer.new_trends.new_trending_tags.no_approved_tags') %> +<% end %> + +<%= raw t('application_mailer.view')%> <%= admin_trends_tags_url(pending_review: '1') %> diff --git a/app/views/admin_mailer/new_trending_links.text.erb b/app/views/admin_mailer/new_trending_links.text.erb deleted file mode 100644 index 51789aca5..000000000 --- a/app/views/admin_mailer/new_trending_links.text.erb +++ /dev/null @@ -1,16 +0,0 @@ -<%= raw t('application_mailer.salutation', name: display_name(@me)) %> - -<%= raw t('admin_mailer.new_trending_links.body') %> - -<% @links.each do |link| %> -- <%= link.title %> • <%= link.url %> - <%= t('admin.trends.links.usage_comparison', today: link.history.get(Time.now.utc).accounts, yesterday: link.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: Trends.links.score(link.id).round(2)) %> -<% end %> - -<% if @lowest_trending_link %> -<%= t('admin_mailer.new_trending_links.requirements', lowest_link_title: @lowest_trending_link.title, lowest_link_score: Trends.links.score(@lowest_trending_link.id).round(2)) %> -<% else %> -<%= t('admin_mailer.new_trending_links.no_approved_links') %> -<% end %> - -<%= raw t('application_mailer.view')%> <%= admin_trends_links_url %> diff --git a/app/views/admin_mailer/new_trending_tags.text.erb b/app/views/admin_mailer/new_trending_tags.text.erb deleted file mode 100644 index 9ea31fa7c..000000000 --- a/app/views/admin_mailer/new_trending_tags.text.erb +++ /dev/null @@ -1,16 +0,0 @@ -<%= raw t('application_mailer.salutation', name: display_name(@me)) %> - -<%= raw t('admin_mailer.new_trending_tags.body') %> - -<% @tags.each do |tag| %> -- #<%= tag.name %> - <%= t('admin.trends.tags.usage_comparison', today: tag.history.get(Time.now.utc).accounts, yesterday: tag.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: Trends.tags.score(tag.id).round(2)) %> -<% end %> - -<% if @lowest_trending_tag %> -<%= t('admin_mailer.new_trending_tags.requirements', lowest_tag_name: @lowest_trending_tag.name, lowest_tag_score: Trends.tags.score(@lowest_trending_tag.id).round(2)) %> -<% else %> -<%= t('admin_mailer.new_trending_tags.no_approved_tags') %> -<% end %> - -<%= raw t('application_mailer.view')%> <%= admin_trends_tags_url(status: 'pending_review') %> diff --git a/app/views/admin_mailer/new_trends.text.erb b/app/views/admin_mailer/new_trends.text.erb new file mode 100644 index 000000000..13b296846 --- /dev/null +++ b/app/views/admin_mailer/new_trends.text.erb @@ -0,0 +1,13 @@ +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> + +<%= raw t('admin_mailer.new_trends.body') %> + +<% unless @links.empty? %> +<%= render 'new_trending_links' %> +<% end %> +<% unless @tags.empty? %> +<%= render 'new_trending_tags' unless @tags.empty? %> +<% end %> +<% unless @statuses.empty? %> +<%= render 'new_trending_statuses' unless @statuses.empty? %> +<% end %> diff --git a/app/views/application/_sidebar.html.haml b/app/views/application/_sidebar.html.haml index 6826c3b58..e97c493fe 100644 --- a/app/views/application/_sidebar.html.haml +++ b/app/views/application/_sidebar.html.haml @@ -6,7 +6,7 @@ %p= @instance_presenter.site_short_description.html_safe.presence || t('about.about_mastodon_html') - if Setting.trends && !(user_signed_in? && !current_user.setting_trends) - - trends = Trends.tags.get(true, 3) + - trends = Trends.tags.query.allowed.limit(3) - unless trends.empty? .endorsements-widget.trends-widget diff --git a/app/workers/scheduler/follow_recommendations_scheduler.rb b/app/workers/scheduler/follow_recommendations_scheduler.rb index 084619cbd..57f78170e 100644 --- a/app/workers/scheduler/follow_recommendations_scheduler.rb +++ b/app/workers/scheduler/follow_recommendations_scheduler.rb @@ -18,7 +18,7 @@ class Scheduler::FollowRecommendationsScheduler fallback_recommendations = FollowRecommendation.order(rank: :desc).limit(SET_SIZE) - I18n.available_locales.map { |locale| locale.to_s.split(/[_-]/).first }.uniq.each do |locale| + Trends.available_locales.each do |locale| recommendations = begin if AccountSummary.safe.filtered.localized(locale).exists? # We can skip the work if no accounts with that language exist FollowRecommendation.localized(locale).order(rank: :desc).limit(SET_SIZE).map { |recommendation| [recommendation.account_id, recommendation.rank] } @@ -49,11 +49,11 @@ class Scheduler::FollowRecommendationsScheduler end end - redis.pipelined do - redis.del(key(locale)) + redis.multi do |multi| + multi.del(key(locale)) recommendations.each do |(account_id, rank)| - redis.zadd(key(locale), rank, account_id) + multi.zadd(key(locale), rank, account_id) end end end diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 6ffe12ae0..c24146da4 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -7,7 +7,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "app/models/status.rb", - "line": 104, + "line": 105, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "result.joins(\"INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}\")", "render_path": null, @@ -20,6 +20,26 @@ "confidence": "Weak", "note": "" }, + { + "warning_type": "SQL Injection", + "warning_code": 0, + "fingerprint": "30dfe36e87fe1b8f239df9a33d576e44a9863f73b680198d4713be6540ae61d3", + "check_name": "SQL", + "message": "Possible SQL injection", + "file": "app/models/trends/query.rb", + "line": 60, + "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", + "code": "klass.joins(\"join unnest(array[#{ids.join(\",\")}]) with ordinality as x (id, ordering) on #{klass.table_name}.id = x.id\")", + "render_path": null, + "location": { + "type": "method", + "class": "Trends::Query", + "method": "to_arel" + }, + "user_input": "ids.join(\",\")", + "confidence": "Weak", + "note": "" + }, { "warning_type": "Redirect", "warning_code": 18, @@ -100,26 +120,6 @@ "confidence": "High", "note": "" }, - { - "warning_type": "SQL Injection", - "warning_code": 0, - "fingerprint": "8c1d8c4b76c1cd3960e90dff999f854a6ff742fcfd8de6c7184ac5a1b1a4d7dd", - "check_name": "SQL", - "message": "Possible SQL injection", - "file": "app/models/preview_card_filter.rb", - "line": 50, - "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", - "code": "PreviewCard.joins(\"join unnest(array[#{(Trends.links.currently_trending_ids(true, -1) or Trends.links.currently_trending_ids(false, -1)).map(&:to_i).join(\",\")}]::integer[]) with ordinality as x (id, ordering) on preview_cards.id = x.id\")", - "render_path": null, - "location": { - "type": "method", - "class": "PreviewCardFilter", - "method": "trending_scope" - }, - "user_input": "(Trends.links.currently_trending_ids(true, -1) or Trends.links.currently_trending_ids(false, -1)).map(&:to_i).join(\",\")", - "confidence": "Medium", - "note": "" - }, { "warning_type": "Cross-Site Scripting", "warning_code": 2, @@ -134,7 +134,7 @@ { "type": "template", "name": "admin/disputes/appeals/index", - "line": 16, + "line": 20, "file": "app/views/admin/disputes/appeals/index.html.haml", "rendered": { "name": "admin/disputes/appeals/_appeal", @@ -170,26 +170,6 @@ "confidence": "High", "note": "" }, - { - "warning_type": "SQL Injection", - "warning_code": 0, - "fingerprint": "c32a484ccd9da46abd3bc93d08b72029d7dbc0576ccf4e878a9627e9a83cad2e", - "check_name": "SQL", - "message": "Possible SQL injection", - "file": "app/models/tag_filter.rb", - "line": 50, - "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", - "code": "Tag.joins(\"join unnest(array[#{Trends.tags.currently_trending_ids(false, -1).map(&:to_i).join(\",\")}]::integer[]) with ordinality as x (id, ordering) on tags.id = x.id\")", - "render_path": null, - "location": { - "type": "method", - "class": "TagFilter", - "method": "trending_scope" - }, - "user_input": "Trends.tags.currently_trending_ids(false, -1).map(&:to_i).join(\",\")", - "confidence": "Medium", - "note": "" - }, { "warning_type": "Cross-Site Scripting", "warning_code": 4, @@ -204,7 +184,7 @@ { "type": "template", "name": "admin/trends/links/index", - "line": 39, + "line": 45, "file": "app/views/admin/trends/links/index.html.haml", "rendered": { "name": "admin/trends/links/_preview_card", @@ -241,6 +221,6 @@ "note": "" } ], - "updated": "2022-02-13 02:24:12 +0100", + "updated": "2022-02-15 03:48:53 +0100", "brakeman_version": "5.2.1" } diff --git a/config/locales/en.yml b/config/locales/en.yml index f045174a9..60c291540 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -787,6 +787,15 @@ en: rejected: Links from this publisher won't trend title: Publishers rejected: Rejected + statuses: + allow: Allow post + allow_account: Allow author + disallow: Disallow post + disallow_account: Disallow author + shared_by: + one: Shared or favourited one time + other: Shared and favourited %{friendly_count} times + title: Trending posts tags: current_score: Current score %{score} dashboard: @@ -835,16 +844,21 @@ en: body: "%{reporter} has reported %{target}" body_remote: Someone from %{domain} has reported %{target} subject: New report for %{instance} (#%{id}) - new_trending_links: - body: The following links are trending today, but their publishers have not been previously reviewed. They will not be displayed publicly unless you approve them. Further notifications from the same publishers will not be generated. - no_approved_links: There are currently no approved trending links. - requirements: The lowest approved trending link is currently "%{lowest_link_title}" with a score of %{lowest_link_score}. - subject: New trending links up for review on %{instance} - new_trending_tags: - body: 'The following hashtags are trending today, but they have not been previously reviewed. They will not be displayed publicly unless you approve them:' - no_approved_tags: There are currently no approved trending hashtags. - requirements: 'The lowest approved trending hashtag is currently #%{lowest_tag_name} with a score of %{lowest_tag_score}.' - subject: New trending hashtags up for review on %{instance} + new_trends: + body: 'The following items need a review before they can be displayed publicly:' + new_trending_links: + no_approved_links: There are currently no approved trending links. + requirements: 'Any of these candidates could surpass the #%{rank} approved trending link, which is currently "%{lowest_link_title}" with a score of %{lowest_link_score}.' + title: Trending links + new_trending_statuses: + no_approved_statuses: There are currently no approved trending posts. + requirements: 'Any of these candidates could surpass the #%{rank} approved trending post, which is currently %{lowest_status_url} with a score of %{lowest_status_score}.' + title: Trending posts + new_trending_tags: + no_approved_tags: There are currently no approved trending hashtags. + requirements: 'Any of these candidates could surpass the #%{rank} approved trending hashtag, which is currently #%{lowest_tag_name} with a score of %{lowest_tag_score}.' + title: Trending hashtags + subject: New trends up for review on %{instance} aliases: add_new: Create alias created_msg: Successfully created a new alias. You can now initiate the move from the old account. diff --git a/config/navigation.rb b/config/navigation.rb index 3fc3747d5..620f78c57 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -34,6 +34,7 @@ SimpleNavigation::Configuration.run do |navigation| n.item :development, safe_join([fa_icon('code fw'), t('settings.development')]), settings_applications_url, if: -> { current_user.functional? } n.item :trends, safe_join([fa_icon('fire fw'), t('admin.trends.title')]), admin_trends_tags_path, if: proc { current_user.staff? } do |s| + s.item :statuses, safe_join([fa_icon('comments-o fw'), t('admin.trends.statuses.title')]), admin_trends_statuses_path, highlights_on: %r{/admin/trends/statuses} s.item :tags, safe_join([fa_icon('hashtag fw'), t('admin.trends.tags.title')]), admin_trends_tags_path, highlights_on: %r{/admin/tags|/admin/trends/tags} s.item :links, safe_join([fa_icon('newspaper-o fw'), t('admin.trends.links.title')]), admin_trends_links_path, highlights_on: %r{/admin/trends/links} end diff --git a/config/routes.rb b/config/routes.rb index 176438e45..a820f32ad 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -327,6 +327,12 @@ Rails.application.routes.draw do end end + resources :statuses, only: [:index] do + collection do + post :batch + end + end + namespace :links do resources :preview_card_providers, only: [:index], path: :publishers do collection do @@ -448,6 +454,7 @@ Rails.application.routes.draw do namespace :trends do resources :links, only: [:index] resources :tags, only: [:index] + resources :statuses, only: [:index] end namespace :emails do @@ -554,6 +561,8 @@ Rails.application.routes.draw do namespace :trends do resources :tags, only: [:index] + resources :links, only: [:index] + resources :statuses, only: [:index] end post :measures, to: 'measures#create' diff --git a/db/migrate/20220202200743_add_trendable_to_accounts.rb b/db/migrate/20220202200743_add_trendable_to_accounts.rb new file mode 100644 index 000000000..414df5108 --- /dev/null +++ b/db/migrate/20220202200743_add_trendable_to_accounts.rb @@ -0,0 +1,7 @@ +class AddTrendableToAccounts < ActiveRecord::Migration[6.1] + def change + add_column :accounts, :trendable, :boolean + add_column :accounts, :reviewed_at, :datetime + add_column :accounts, :requested_review_at, :datetime + end +end diff --git a/db/migrate/20220202200926_add_trendable_to_statuses.rb b/db/migrate/20220202200926_add_trendable_to_statuses.rb new file mode 100644 index 000000000..7f38c8ca7 --- /dev/null +++ b/db/migrate/20220202200926_add_trendable_to_statuses.rb @@ -0,0 +1,5 @@ +class AddTrendableToStatuses < ActiveRecord::Migration[6.1] + def change + add_column :statuses, :trendable, :boolean + end +end diff --git a/db/post_migrate/20220202201015_remove_trust_level_from_accounts.rb b/db/post_migrate/20220202201015_remove_trust_level_from_accounts.rb new file mode 100644 index 000000000..d5d995ece --- /dev/null +++ b/db/post_migrate/20220202201015_remove_trust_level_from_accounts.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class RemoveTrustLevelFromAccounts < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + safety_assured { remove_column :accounts, :trust_level, :integer } + end +end diff --git a/db/schema.rb b/db/schema.rb index 0e9b6e619..e54de5b37 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -177,13 +177,15 @@ ActiveRecord::Schema.define(version: 2022_02_24_010024) do t.string "also_known_as", array: true t.datetime "silenced_at" t.datetime "suspended_at" - t.integer "trust_level" t.boolean "hide_collections" t.integer "avatar_storage_schema_version" t.integer "header_storage_schema_version" t.string "devices_url" t.integer "suspension_origin" t.datetime "sensitized_at" + t.boolean "trendable" + t.datetime "reviewed_at" + t.datetime "requested_review_at" t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id" @@ -887,6 +889,7 @@ ActiveRecord::Schema.define(version: 2022_02_24_010024) do t.bigint "poll_id" t.datetime "deleted_at" t.datetime "edited_at" + t.boolean "trendable" t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)" t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)" t.index ["id", "account_id"], name: "index_statuses_local_20190824", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))" @@ -1228,5 +1231,4 @@ ActiveRecord::Schema.define(version: 2022_02_24_010024) do ORDER BY (sum(t0.rank)) DESC; SQL add_index "follow_recommendations", ["account_id"], name: "index_follow_recommendations_on_account_id", unique: true - end diff --git a/spec/controllers/api/v1/trends/tags_controller_spec.rb b/spec/controllers/api/v1/trends/tags_controller_spec.rb index e2e26dcab..d29551c56 100644 --- a/spec/controllers/api/v1/trends/tags_controller_spec.rb +++ b/spec/controllers/api/v1/trends/tags_controller_spec.rb @@ -7,10 +7,9 @@ RSpec.describe Api::V1::Trends::TagsController, type: :controller do describe 'GET #index' do before do - trending_tags = double() - - allow(trending_tags).to receive(:get).and_return(Fabricate.times(10, :tag)) - allow(Trends).to receive(:tags).and_return(trending_tags) + Fabricate.times(10, :tag).each do |tag| + 10.times { |i| Trends.tags.add(tag, i) } + end get :index end diff --git a/spec/mailers/previews/admin_mailer_preview.rb b/spec/mailers/previews/admin_mailer_preview.rb index 9c0372b47..01436ba7a 100644 --- a/spec/mailers/previews/admin_mailer_preview.rb +++ b/spec/mailers/previews/admin_mailer_preview.rb @@ -6,14 +6,9 @@ class AdminMailerPreview < ActionMailer::Preview AdminMailer.new_pending_account(Account.first, User.pending.first) end - # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_trending_tags - def new_trending_tags - AdminMailer.new_trending_tags(Account.first, Tag.limit(3)) - end - - # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_trending_links - def new_trending_links - AdminMailer.new_trending_links(Account.first, PreviewCard.limit(3)) + # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_trends + def new_trends + AdminMailer.new_trends(Account.first, PreviewCard.limit(3), Tag.limit(3), Status.where(reblog_of_id: nil).limit(3)) end # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_appeal diff --git a/spec/models/trends/statuses_spec.rb b/spec/models/trends/statuses_spec.rb new file mode 100644 index 000000000..9cc67acbe --- /dev/null +++ b/spec/models/trends/statuses_spec.rb @@ -0,0 +1,110 @@ +require 'rails_helper' + +RSpec.describe Trends::Statuses do + subject! { described_class.new(threshold: 5, review_threshold: 10, score_halflife: 8.hours) } + + let!(:at_time) { DateTime.new(2021, 11, 14, 10, 15, 0) } + + describe 'Trends::Statuses::Query' do + let!(:query) { subject.query } + let!(:today) { at_time } + + let!(:status1) { Fabricate(:status, text: 'Foo', trendable: true, created_at: today) } + let!(:status2) { Fabricate(:status, text: 'Bar', trendable: true, created_at: today) } + + before do + 15.times { reblog(status1, today) } + 12.times { reblog(status2, today) } + + subject.refresh(today) + end + + describe '#filtered_for' do + let(:account) { Fabricate(:account) } + + it 'returns a composable query scope' do + expect(query.filtered_for(account)).to be_a Trends::Query + end + + it 'filters out blocked accounts' do + account.block!(status1.account) + expect(query.filtered_for(account).to_a).to eq [status2] + end + + it 'filters out muted accounts' do + account.mute!(status2.account) + expect(query.filtered_for(account).to_a).to eq [status1] + end + + it 'filters out blocked-by accounts' do + status1.account.block!(account) + expect(query.filtered_for(account).to_a).to eq [status2] + end + end + end + + describe '#add' do + let(:status) { Fabricate(:status) } + + before do + subject.add(status, 1, at_time) + end + + it 'records use' do + expect(subject.send(:recently_used_ids, at_time)).to eq [status.id] + end + end + + describe '#query' do + it 'returns a composable query scope' do + expect(subject.query).to be_a Trends::Query + end + + it 'responds to filtered_for' do + expect(subject.query).to respond_to(:filtered_for) + end + end + + describe '#refresh' do + let!(:today) { at_time } + let!(:yesterday) { today - 1.day } + + let!(:status1) { Fabricate(:status, text: 'Foo', trendable: true, created_at: yesterday) } + let!(:status2) { Fabricate(:status, text: 'Bar', trendable: true, created_at: today) } + let!(:status3) { Fabricate(:status, text: 'Baz', trendable: true, created_at: today) } + + before do + 13.times { reblog(status1, today) } + 13.times { reblog(status2, today) } + 4.times { reblog(status3, today) } + end + + context do + before do + subject.refresh(today) + end + + it 'calculates and re-calculates scores' do + expect(subject.query.limit(10).to_a).to eq [status2, status1] + end + + it 'omits statuses below threshold' do + expect(subject.query.limit(10).to_a).to_not include(status3) + end + end + + it 'decays scores' do + subject.refresh(today) + original_score = subject.score(status2.id) + expect(original_score).to be_a Float + subject.refresh(today + subject.options[:score_halflife]) + decayed_score = subject.score(status2.id) + expect(decayed_score).to be <= original_score / 2 + end + end + + def reblog(status, at_time) + reblog = Fabricate(:status, reblog: status, created_at: at_time) + subject.add(status, reblog.account_id, at_time) + end +end diff --git a/spec/models/trends/tags_spec.rb b/spec/models/trends/tags_spec.rb index 4f98c6aa4..f48c73503 100644 --- a/spec/models/trends/tags_spec.rb +++ b/spec/models/trends/tags_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Trends::Tags do end end - describe '#get' do + describe '#query' do pending end @@ -47,11 +47,11 @@ RSpec.describe Trends::Tags do end it 'calculates and re-calculates scores' do - expect(subject.get(false, 10)).to eq [tag1, tag3] + expect(subject.query.limit(10).to_a).to eq [tag1, tag3] end it 'omits hashtags below threshold' do - expect(subject.get(false, 10)).to_not include(tag2) + expect(subject.query.limit(10).to_a).to_not include(tag2) end end -- cgit From 02b8d63fcef2d30e2514111ec89308a9435dd2ed Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 2 Mar 2022 18:57:08 +0100 Subject: Fix report category not being saved in REST API (#17682) --- app/controllers/api/v1/reports_controller.rb | 12 +---- app/services/report_service.rb | 14 ++++-- spec/controllers/api/v1/reports_controller_spec.rb | 54 +++++++++++++++++++--- spec/fabricators/rule_fabricator.rb | 8 ++-- 4 files changed, 62 insertions(+), 26 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 052d70cc8..8ff6c8fe5 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -10,9 +10,7 @@ class Api::V1::ReportsController < Api::BaseController @report = ReportService.new.call( current_account, reported_account, - status_ids: reported_status_ids, - comment: report_params[:comment], - forward: report_params[:forward] + report_params ) render json: @report, serializer: REST::ReportSerializer @@ -20,14 +18,6 @@ class Api::V1::ReportsController < Api::BaseController private - def reported_status_ids - reported_account.statuses.with_discarded.find(status_ids).pluck(:id) - end - - def status_ids - Array(report_params[:status_ids]) - end - def reported_account Account.find(report_params[:account_id]) end diff --git a/app/services/report_service.rb b/app/services/report_service.rb index caf99ab6e..9d784c341 100644 --- a/app/services/report_service.rb +++ b/app/services/report_service.rb @@ -6,10 +6,10 @@ class ReportService < BaseService def call(source_account, target_account, options = {}) @source_account = source_account @target_account = target_account - @status_ids = options.delete(:status_ids) || [] - @comment = options.delete(:comment) || '' - @category = options.delete(:category) || 'other' - @rule_ids = options.delete(:rule_ids) + @status_ids = options.delete(:status_ids).presence || [] + @comment = options.delete(:comment).presence || '' + @category = options.delete(:category).presence || 'other' + @rule_ids = options.delete(:rule_ids).presence @options = options raise ActiveRecord::RecordNotFound if @target_account.suspended? @@ -26,7 +26,7 @@ class ReportService < BaseService def create_report! @report = @source_account.reports.create!( target_account: @target_account, - status_ids: @status_ids, + status_ids: reported_status_ids, comment: @comment, uri: @options[:uri], forwarded: forward?, @@ -56,6 +56,10 @@ class ReportService < BaseService !@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward]) end + def reported_status_ids + @target_account.statuses.with_discarded.find(Array(@status_ids)).pluck(:id) + end + def payload Oj.dump(serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account)) end diff --git a/spec/controllers/api/v1/reports_controller_spec.rb b/spec/controllers/api/v1/reports_controller_spec.rb index a13de1370..b5baf60e1 100644 --- a/spec/controllers/api/v1/reports_controller_spec.rb +++ b/spec/controllers/api/v1/reports_controller_spec.rb @@ -13,22 +13,64 @@ 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) } + let!(:admin) { Fabricate(:user, admin: true) } + + let(:scopes) { 'write:reports' } + let(:status) { Fabricate(:status) } + let(:target_account) { status.account } + let(:category) { nil } + let(:forward) { nil } + let(:rule_ids){ nil } before do allow(AdminMailer).to receive(:new_report).and_return(double('email', deliver_later: nil)) - post :create, params: { status_ids: [status.id], account_id: status.account.id, comment: 'reasons' } + post :create, params: { status_ids: [status.id], account_id: target_account.id, comment: 'reasons', category: category, rule_ids: rule_ids, forward: forward } end - it 'creates a report' do - expect(status.reload.account.targeted_reports).not_to be_empty + it 'returns http success' do expect(response).to have_http_status(200) end + it 'creates a report' do + expect(target_account.targeted_reports).to_not be_empty + end + + it 'saves comment' do + expect(target_account.targeted_reports.first.comment).to eq 'reasons' + end + it 'sends e-mails to admins' do expect(AdminMailer).to have_received(:new_report).with(admin.account, Report) end + + context 'when a status does not belong to the reported account' do + let(:target_account) { Fabricate(:account) } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'when a category is chosen' do + let(:category) { 'spam' } + + it 'saves category' do + expect(target_account.targeted_reports.first.spam?).to be true + end + end + + context 'when violated rules are chosen' do + let(:rule) { Fabricate(:rule) } + let(:category) { 'violation' } + let(:rule_ids) { [rule.id] } + + it 'saves category' do + expect(target_account.targeted_reports.first.violation?).to be true + end + + it 'saves rule_ids' do + expect(target_account.targeted_reports.first.rule_ids).to match_array([rule.id]) + end + end end end diff --git a/spec/fabricators/rule_fabricator.rb b/spec/fabricators/rule_fabricator.rb index 4bdfd05e0..bc29bc48e 100644 --- a/spec/fabricators/rule_fabricator.rb +++ b/spec/fabricators/rule_fabricator.rb @@ -1,5 +1,5 @@ Fabricator(:rule) do - priority "" - deleted_at "2021-02-21 05:51:09" - text "MyText" -end \ No newline at end of file + priority 0 + deleted_at nil + text { Faker::Lorem.paragraph } +end -- cgit From b5329e0035d455e72dad7249d88bd624b5cb59a0 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Sun, 6 Mar 2022 16:51:40 -0500 Subject: Spelling (#17705) * spelling: account Signed-off-by: Josh Soref * spelling: affiliated Signed-off-by: Josh Soref * spelling: appearance Signed-off-by: Josh Soref * spelling: autosuggest Signed-off-by: Josh Soref * spelling: cacheable Signed-off-by: Josh Soref * spelling: component Signed-off-by: Josh Soref * spelling: conversations Signed-off-by: Josh Soref * spelling: domain.example Clarify what's distinct and use RFC friendly domain space. Signed-off-by: Josh Soref * spelling: environment Signed-off-by: Josh Soref * spelling: exceeds Signed-off-by: Josh Soref * spelling: functional Signed-off-by: Josh Soref * spelling: inefficiency Signed-off-by: Josh Soref * spelling: not Signed-off-by: Josh Soref * spelling: notifications Signed-off-by: Josh Soref * spelling: occurring Signed-off-by: Josh Soref * spelling: position Signed-off-by: Josh Soref * spelling: progress Signed-off-by: Josh Soref * spelling: promotable Signed-off-by: Josh Soref * spelling: reblogging Signed-off-by: Josh Soref * spelling: repetitive Signed-off-by: Josh Soref * spelling: resolve Signed-off-by: Josh Soref * spelling: saturated Signed-off-by: Josh Soref * spelling: similar Signed-off-by: Josh Soref * spelling: strategies Signed-off-by: Josh Soref * spelling: success Signed-off-by: Josh Soref * spelling: targeting Signed-off-by: Josh Soref * spelling: thumbnails Signed-off-by: Josh Soref * spelling: unauthorized Signed-off-by: Josh Soref * spelling: unsensitizes Signed-off-by: Josh Soref * spelling: validations Signed-off-by: Josh Soref * spelling: various Signed-off-by: Josh Soref Co-authored-by: Josh Soref --- .circleci/config.yml | 2 +- .env.production.sample | 2 +- CHANGELOG.md | 20 ++++++++--------- .../mastodon/components/scrollable_list.js | 2 +- .../mastodon/containers/media_container.js | 4 ++-- .../mastodon/features/report/category.js | 2 +- app/javascript/mastodon/features/video/index.js | 4 ++-- app/javascript/mastodon/locales/en.json | 2 +- app/javascript/styles/mastodon/components.scss | 2 +- app/javascript/styles/mastodon/polls.scss | 2 +- app/policies/user_policy.rb | 4 ++-- config/initializers/omniauth.rb | 2 +- config/locales/en_GB.yml | 2 +- db/migrate/20170920032311_fix_reblogs_in_feeds.rb | 2 +- ...0180608213548_reject_following_blocked_users.rb | 6 ++--- lib/mastodon/maintenance_cli.rb | 2 +- lib/mastodon/statuses_cli.rb | 2 +- spec/controllers/accounts_controller_spec.rb | 8 +++---- .../activitypub/collections_controller_spec.rb | 6 ++--- .../activitypub/outboxes_controller_spec.rb | 6 ++--- .../activitypub/replies_controller_spec.rb | 4 ++-- .../api/v1/accounts/notes_controller_spec.rb | 2 +- .../api/v1/admin/accounts_controller_spec.rb | 2 +- .../favourited_by_accounts_controller_spec.rb | 2 +- .../reblogged_by_accounts_controller_spec.rb | 2 +- .../controllers/api/v1/statuses_controller_spec.rb | 4 ++-- spec/controllers/application_controller_spec.rb | 20 ++++++++--------- spec/controllers/statuses_controller_spec.rb | 6 ++--- spec/helpers/application_helper_spec.rb | 2 +- spec/lib/tag_manager_spec.rb | 26 +++++++++++----------- spec/models/report_spec.rb | 2 +- spec/policies/user_policy_spec.rb | 4 ++-- spec/services/unsuspend_account_service_spec.rb | 16 ++++++------- spec/support/stories/profile_stories.rb | 2 +- 34 files changed, 88 insertions(+), 88 deletions(-) (limited to 'spec/controllers') diff --git a/.circleci/config.yml b/.circleci/config.yml index e157f8f22..318f4d2be 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -153,7 +153,7 @@ jobs: - run: command: ./bin/rails db:migrate name: Run all pre-deployment migrations - evironment: + environment: SKIP_POST_DEPLOYMENT_MIGRATIONS: true - run: command: ./bin/rails db:migrate diff --git a/.env.production.sample b/.env.production.sample index 8eeff3794..4fc58072f 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -58,7 +58,7 @@ SMTP_SERVER=smtp.mailgun.org SMTP_PORT=587 SMTP_LOGIN= SMTP_PASSWORD= -SMTP_FROM_ADDRESS=notificatons@example.com +SMTP_FROM_ADDRESS=notifications@example.com # File storage (optional) # ----------------------- diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e9d6ea1d..d8fde27fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,7 +87,7 @@ All notable changes to this project will be documented in this file. - Fix suspended accounts statuses being merged back into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16628)) - Fix crash when encountering invalid account fields ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16598)) - Fix invalid blurhash handling for remote activities ([noellabo](https://github.com/mastodon/mastodon/pull/16583)) -- Fix newlines being added to accout notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576)) +- Fix newlines being added to account notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576)) - Fix crash when creating an announcement with links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16941)) - Fix logging out from one browser logging out all other sessions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943)) @@ -420,7 +420,7 @@ All notable changes to this project will be documented in this file. - Fix inefficiency when fetching bookmarks ([akihikodaki](https://github.com/mastodon/mastodon/pull/14674)) - Fix inefficiency when fetching favourites ([akihikodaki](https://github.com/mastodon/mastodon/pull/14673)) - Fix inefficiency when fetching media-only account timeline ([akihikodaki](https://github.com/mastodon/mastodon/pull/14675)) -- Fix inefficieny when deleting accounts ([Gargron](https://github.com/mastodon/mastodon/pull/15387), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15407), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15408), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15402), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15416), [Gargron](https://github.com/mastodon/mastodon/pull/15421)) +- Fix inefficiency when deleting accounts ([Gargron](https://github.com/mastodon/mastodon/pull/15387), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15407), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15408), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15402), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15416), [Gargron](https://github.com/mastodon/mastodon/pull/15421)) - Fix redundant query when processing batch actions on custom emojis ([niwatori24](https://github.com/mastodon/mastodon/pull/14534)) - Fix slow distinct queries where grouped queries are faster ([Gargron](https://github.com/mastodon/mastodon/pull/15287)) - Fix performance on instances list in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/15282)) @@ -507,7 +507,7 @@ All notable changes to this project will be documented in this file. - Add blurhash to link previews ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13984), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14143), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13985), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14267), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14278), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14126), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14261), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14260)) - In web UI, toots cannot be marked as sensitive unless there is media attached - However, it's possible to do via API or ActivityPub - - Thumnails of link previews of such posts now use blurhash in web UI + - Thumbnails of link previews of such posts now use blurhash in web UI - The Card entity in REST API has a new `blurhash` attribute - Add support for `summary` field for media description in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13763)) - Add hints about incomplete remote content to web UI ([Gargron](https://github.com/mastodon/mastodon/pull/14031), [noellabo](https://github.com/mastodon/mastodon/pull/14195)) @@ -530,7 +530,7 @@ All notable changes to this project will be documented in this file. - The `meta` attribute on the Media Attachment entity in REST API can now have a `colors` attribute which in turn contains three hex colors: `background`, `foreground`, and `accent` - The background color is chosen from the most dominant color around the edges of the thumbnail - The foreground and accent colors are chosen from the colors that are the most different from the background color using the CIEDE2000 algorithm - - The most satured color of the two is designated as the accent color + - The most saturated color of the two is designated as the accent color - The one with the highest W3C contrast is designated as the foreground color - If there are not enough colors in the thumbnail, new ones are generated using a monochrome pattern - Add a visibility indicator to toots in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/14123), [highemerly](https://github.com/mastodon/mastodon/pull/14292)) @@ -556,7 +556,7 @@ All notable changes to this project will be documented in this file. - Change boost button to no longer serve as visibility indicator in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/14132), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14373)) - Change contrast of flash messages ([cchoi12](https://github.com/mastodon/mastodon/pull/13892)) - Change wording from "Hide media" to "Hide image/images" in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13834)) -- Change appearence of settings pages to be more consistent ([ariasuni](https://github.com/mastodon/mastodon/pull/13938)) +- Change appearance of settings pages to be more consistent ([ariasuni](https://github.com/mastodon/mastodon/pull/13938)) - Change "Add media" tooltip to not include long list of formats in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13954)) - Change how badly contrasting emoji are rendered in web UI ([leo60228](https://github.com/mastodon/mastodon/pull/13773), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13772), [mfmfuyu](https://github.com/mastodon/mastodon/pull/14020), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14015)) - Change structure of unavailable content section on about page ([ariasuni](https://github.com/mastodon/mastodon/pull/13930)) @@ -578,8 +578,8 @@ All notable changes to this project will be documented in this file. ### Fixed - Fix `following` param not working when exact match is found in account search ([noellabo](https://github.com/mastodon/mastodon/pull/14394)) -- Fix sometimes occuring duplicate mention notifications ([noellabo](https://github.com/mastodon/mastodon/pull/14378)) -- Fix RSS feeds not being cachable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14368)) +- Fix sometimes occurring duplicate mention notifications ([noellabo](https://github.com/mastodon/mastodon/pull/14378)) +- Fix RSS feeds not being cacheable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14368)) - Fix lack of locking around processing of Announce activities in ActivityPub ([noellabo](https://github.com/mastodon/mastodon/pull/14365)) - Fix boosted toots from blocked account not being retroactively removed from TL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14339)) - Fix large shortened numbers (like 1.2K) using incorrect pluralization ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14061)) @@ -706,7 +706,7 @@ All notable changes to this project will be documented in this file. - Fix poll refresh button not being debounced in web UI ([rasjonell](https://github.com/mastodon/mastodon/pull/13485), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13490)) - Fix confusing error when failing to add an alias to an unknown account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13480)) - Fix "Email changed" notification sometimes having wrong e-mail ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13475)) -- Fix varioues issues on the account aliases page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13452)) +- Fix various issues on the account aliases page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13452)) - Fix API footer link in web UI ([bubblineyuri](https://github.com/mastodon/mastodon/pull/13441)) - Fix pagination of following, followers, follow requests, blocks and mutes lists in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13445)) - Fix styling of polls in JS-less fallback on public pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13436)) @@ -1496,7 +1496,7 @@ All notable changes to this project will be documented in this file. - Change Docker image to use Ubuntu with jemalloc ([Sir-Boops](https://github.com/mastodon/mastodon/pull/10100), [BenLubar](https://github.com/mastodon/mastodon/pull/10212)) - Change public pages to be cacheable by proxies ([BenLubar](https://github.com/mastodon/mastodon/pull/9059)) - Change the 410 gone response for suspended accounts to be cacheable by proxies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10339)) -- Change web UI to not not empty timeline of blocked users on block ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10359)) +- Change web UI to not empty timeline of blocked users on block ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10359)) - Change JSON serializer to remove unused `@context` values ([Gargron](https://github.com/mastodon/mastodon/pull/10378)) - Change GIFV file size limit to be the same as for other videos ([rinsuki](https://github.com/mastodon/mastodon/pull/9924)) - Change Webpack to not use @babel/preset-env to compile node_modules ([ykzts](https://github.com/mastodon/mastodon/pull/10289)) @@ -1673,7 +1673,7 @@ All notable changes to this project will be documented in this file. - Limit maximum visibility of local silenced users to unlisted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9583)) - Change API error message for unconfirmed accounts ([noellabo](https://github.com/mastodon/mastodon/pull/9625)) - Change the icon to "reply-all" when it's a reply to other accounts ([mayaeh](https://github.com/mastodon/mastodon/pull/9378)) -- Do not ignore federated reports targetting already-reported accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9534)) +- Do not ignore federated reports targeting already-reported accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9534)) - Upgrade default Ruby version to 2.6.0 ([Gargron](https://github.com/mastodon/mastodon/pull/9688)) - Change e-mail digest frequency ([Gargron](https://github.com/mastodon/mastodon/pull/9689)) - Change Docker images for Tor support in docker-compose.yml ([Sir-Boops](https://github.com/mastodon/mastodon/pull/9438)) diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index 68a178512..91d04bf4d 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -151,7 +151,7 @@ class ScrollableList extends PureComponent { attachFullscreenListener(this.onFullScreenChange); - // Handle initial scroll posiiton + // Handle initial scroll position this.handleScroll(); } diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js index 2f42a084f..6ee1f0bd8 100644 --- a/app/javascript/mastodon/containers/media_container.js +++ b/app/javascript/mastodon/containers/media_container.js @@ -43,7 +43,7 @@ export default class MediaContainer extends PureComponent { handleOpenVideo = (options) => { const { components } = this.props; - const { media } = JSON.parse(components[options.componetIndex].getAttribute('data-props')); + const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props')); const mediaList = fromJS(media); document.body.classList.add('with-modals--active'); @@ -87,7 +87,7 @@ export default class MediaContainer extends PureComponent { ...(hashtag ? { hashtag: fromJS(hashtag) } : {}), ...(componentName === 'Video' ? { - componetIndex: i, + componentIndex: i, onOpenVideo: this.handleOpenVideo, } : { onOpenMedia: this.handleOpenMedia, diff --git a/app/javascript/mastodon/features/report/category.js b/app/javascript/mastodon/features/report/category.js index 122b51c7c..a36dc81b1 100644 --- a/app/javascript/mastodon/features/report/category.js +++ b/app/javascript/mastodon/features/report/category.js @@ -8,7 +8,7 @@ const messages = defineMessages({ dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' }, dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' }, spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' }, - spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetetive replies' }, + spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetitive replies' }, violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' }, violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' }, other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' }, diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 70e3cd6e8..8d47e479a 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -121,7 +121,7 @@ class Video extends React.PureComponent { autoPlay: PropTypes.bool, volume: PropTypes.number, muted: PropTypes.bool, - componetIndex: PropTypes.number, + componentIndex: PropTypes.number, }; static defaultProps = { @@ -502,7 +502,7 @@ class Video extends React.PureComponent { startTime: this.video.currentTime, autoPlay: !this.state.paused, defaultVolume: this.state.volume, - componetIndex: this.props.componetIndex, + componentIndex: this.props.componentIndex, }); } diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index e87541e28..c3d23795b 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -409,7 +409,7 @@ "report.reasons.other": "It's something else", "report.reasons.other_description": "The issue does not fit into other categories", "report.reasons.spam": "It's spam", - "report.reasons.spam_description": "Malicious links, fake engagement, or repetetive replies", + "report.reasons.spam_description": "Malicious links, fake engagement, or repetitive replies", "report.reasons.violation": "It violates server rules", "report.reasons.violation_description": "You are aware that it breaks specific rules", "report.rules.subtitle": "Select all that apply", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 6b18ca6f2..b8c3dba61 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4315,7 +4315,7 @@ a.status-card.compact:hover { } } -.upload-progess__message { +.upload-progress__message { flex: 1 1 auto; } diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index e33fc7983..a719044ea 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -69,7 +69,7 @@ display: none; } - .autossugest-input { + .autosuggest-input { flex: 1 1 auto; } diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 6695a0ddf..92e2c4f4b 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -42,7 +42,7 @@ class UserPolicy < ApplicationPolicy end def promote? - admin? && promoteable? + admin? && promotable? end def demote? @@ -51,7 +51,7 @@ class UserPolicy < ApplicationPolicy private - def promoteable? + def promotable? record.approved? && (!record.staff? || !record.admin?) end diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 19d59f155..1a041ad48 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -1,5 +1,5 @@ Rails.application.config.middleware.use OmniAuth::Builder do - # Vanilla omniauth stategies + # Vanilla omniauth strategies end Devise.setup do |config| diff --git a/config/locales/en_GB.yml b/config/locales/en_GB.yml index d3461474b..de74e0f61 100644 --- a/config/locales/en_GB.yml +++ b/config/locales/en_GB.yml @@ -986,7 +986,7 @@ en_GB: enabled: Two-factor authentication is enabled enabled_success: Two-factor authentication successfully enabled generate_recovery_codes: Generate recovery codes - instructions_html: "Scan this QR code into Google Authenticator or a similiar TOTP app on your phone. From now on, that app will generate tokens that you will have to enter when logging in." + instructions_html: "Scan this QR code into Google Authenticator or a similar TOTP app on your phone. From now on, that app will generate tokens that you will have to enter when logging in." lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated. manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:' recovery_codes: Backup recovery codes diff --git a/db/migrate/20170920032311_fix_reblogs_in_feeds.rb b/db/migrate/20170920032311_fix_reblogs_in_feeds.rb index bcd4b9137..4ab68e8f3 100644 --- a/db/migrate/20170920032311_fix_reblogs_in_feeds.rb +++ b/db/migrate/20170920032311_fix_reblogs_in_feeds.rb @@ -16,7 +16,7 @@ class FixReblogsInFeeds < ActiveRecord::Migration[5.1] # is once again set to the reblogging status' ID, and the value # is set to the reblogged status' ID). This is safe for Redis' # float conversion because in this reblog tracking zset, we only - # need the rebloggging status' ID to be able to stop tracking + # need the reblogging status' ID to be able to stop tracking # entries after they have gotten too far down the feed, which # does not require an exact value. diff --git a/db/migrate/20180608213548_reject_following_blocked_users.rb b/db/migrate/20180608213548_reject_following_blocked_users.rb index 302db6b68..78f8df538 100644 --- a/db/migrate/20180608213548_reject_following_blocked_users.rb +++ b/db/migrate/20180608213548_reject_following_blocked_users.rb @@ -22,13 +22,13 @@ class RejectFollowingBlockedUsers < ActiveRecord::Migration[5.2] follows.each do |follow| blocked_account = follow.account - followed_acccount = follow.target_account + followed_account = follow.target_account next follow.destroy! if blocked_account.local? - reject_follow_json = Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(follow, serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).as_json).sign!(followed_acccount)) + reject_follow_json = Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(follow, serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).as_json).sign!(followed_account)) - ActivityPub::DeliveryWorker.perform_async(reject_follow_json, followed_acccount, blocked_account.inbox_url) + ActivityPub::DeliveryWorker.perform_async(reject_follow_json, followed_account, blocked_account.inbox_url) follow.destroy! end diff --git a/lib/mastodon/maintenance_cli.rb b/lib/mastodon/maintenance_cli.rb index 00861df77..1e047d96c 100644 --- a/lib/mastodon/maintenance_cli.rb +++ b/lib/mastodon/maintenance_cli.rb @@ -510,7 +510,7 @@ module Mastodon accounts = accounts.sort_by(&:id).reverse @prompt.warn "Multiple local accounts were found for username '#{accounts.first.username}'." - @prompt.warn 'All those accounts are distinct accounts but only the most recently-created one is fully-functionnal.' + @prompt.warn 'All those accounts are distinct accounts but only the most recently-created one is fully-functional.' accounts.each_with_index do |account, idx| @prompt.say '%2d. %s: created at: %s; updated at: %s; last logged in at: %s; statuses: %5d; last status at: %s' % [idx, account.username, account.created_at, account.updated_at, account.user&.last_sign_in_at&.to_s || 'N/A', account.account_stat&.statuses_count || 0, account.account_stat&.last_status_at || 'N/A'] diff --git a/lib/mastodon/statuses_cli.rb b/lib/mastodon/statuses_cli.rb index 91b08813b..d4c2e6cf2 100644 --- a/lib/mastodon/statuses_cli.rb +++ b/lib/mastodon/statuses_cli.rb @@ -156,7 +156,7 @@ module Mastodon ActiveRecord::Base.connection.add_index(:statuses, :conversation_id, name: :index_statuses_conversation_id, algorithm: :concurrently, if_not_exists: true) - say('Extract the deletion target from coversations... This might take a while...') + say('Extract the deletion target from conversations... This might take a while...') ActiveRecord::Base.connection.create_table('conversations_to_be_deleted', force: true) diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 73d124029..662a89927 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -5,7 +5,7 @@ RSpec.describe AccountsController, type: :controller do let(:account) { Fabricate(:account) } - shared_examples 'cachable response' do + shared_examples 'cacheable response' do it 'does not set cookies' do expect(response.cookies).to be_empty expect(response.headers['Set-Cookies']).to be nil @@ -374,7 +374,7 @@ RSpec.describe AccountsController, type: :controller do expect(response.media_type).to eq 'application/activity+json' end - it_behaves_like 'cachable response' + it_behaves_like 'cacheable response' it 'renders account' do json = body_as_json @@ -432,7 +432,7 @@ RSpec.describe AccountsController, type: :controller do expect(response.media_type).to eq 'application/activity+json' end - it_behaves_like 'cachable response' + it_behaves_like 'cacheable response' it 'renders account' do json = body_as_json @@ -499,7 +499,7 @@ RSpec.describe AccountsController, type: :controller do expect(response).to have_http_status(200) end - it_behaves_like 'cachable response' + it_behaves_like 'cacheable response' end context do diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/controllers/activitypub/collections_controller_spec.rb index 21a033945..4d87f80ce 100644 --- a/spec/controllers/activitypub/collections_controller_spec.rb +++ b/spec/controllers/activitypub/collections_controller_spec.rb @@ -7,7 +7,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) } let(:remote_account) { nil } - shared_examples 'cachable response' do + shared_examples 'cacheable response' do it 'does not set cookies' do expect(response.cookies).to be_empty expect(response.headers['Set-Cookies']).to be nil @@ -48,7 +48,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do expect(response.media_type).to eq 'application/activity+json' end - it_behaves_like 'cachable response' + it_behaves_like 'cacheable response' it 'returns orderedItems with pinned statuses' do expect(body[:orderedItems]).to be_an Array @@ -101,7 +101,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do expect(response.media_type).to eq 'application/activity+json' end - it_behaves_like 'cachable response' + it_behaves_like 'cacheable response' it 'returns orderedItems with pinned statuses' do json = body_as_json diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb index 1722690db..04f036447 100644 --- a/spec/controllers/activitypub/outboxes_controller_spec.rb +++ b/spec/controllers/activitypub/outboxes_controller_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe ActivityPub::OutboxesController, type: :controller do let!(:account) { Fabricate(:account) } - shared_examples 'cachable response' do + shared_examples 'cacheable response' do it 'does not set cookies' do expect(response.cookies).to be_empty expect(response.headers['Set-Cookies']).to be nil @@ -53,7 +53,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do expect(body[:totalItems]).to eq 4 end - it_behaves_like 'cachable response' + it_behaves_like 'cacheable response' it 'does not have a Vary header' do expect(response.headers['Vary']).to be_nil @@ -98,7 +98,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do expect(body[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true end - it_behaves_like 'cachable response' + it_behaves_like 'cacheable response' it 'returns Vary header with Signature' do expect(response.headers['Vary']).to include 'Signature' diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/controllers/activitypub/replies_controller_spec.rb index a2c7f336f..a35957f24 100644 --- a/spec/controllers/activitypub/replies_controller_spec.rb +++ b/spec/controllers/activitypub/replies_controller_spec.rb @@ -8,7 +8,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do let(:remote_reply_id) { 'https://foobar.com/statuses/1234' } let(:remote_querier) { nil } - shared_examples 'cachable response' do + shared_examples 'cacheable response' do it 'does not set cookies' do expect(response.cookies).to be_empty expect(response.headers['Set-Cookies']).to be nil @@ -93,7 +93,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do expect(response.media_type).to eq 'application/activity+json' end - it_behaves_like 'cachable response' + it_behaves_like 'cacheable response' context 'without only_other_accounts' do it "returns items with thread author's replies" do diff --git a/spec/controllers/api/v1/accounts/notes_controller_spec.rb b/spec/controllers/api/v1/accounts/notes_controller_spec.rb index 47d595c70..42c2d8a86 100644 --- a/spec/controllers/api/v1/accounts/notes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/notes_controller_spec.rb @@ -31,7 +31,7 @@ describe Api::V1::Accounts::NotesController do end end - context 'when account note exceends allowed length' do + context 'when account note exceeds allowed length' do let(:comment) { 'a' * 2_001 } it 'returns 422' do diff --git a/spec/controllers/api/v1/admin/accounts_controller_spec.rb b/spec/controllers/api/v1/admin/accounts_controller_spec.rb index bf79ee520..3f61bbc0b 100644 --- a/spec/controllers/api/v1/admin/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/admin/accounts_controller_spec.rb @@ -140,7 +140,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do expect(response).to have_http_status(200) end - it 'unsensitives account' do + it 'unsensitizes account' do expect(account.reload.sensitized?).to be false end end 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 439a4738d..7cc77f430 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 @@ -56,7 +56,7 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control Fabricate(:favourite, status: status) end - it 'returns http unautharized' do + it 'returns http unauthorized' do get :index, params: { status_id: status.id } expect(response).to have_http_status(404) end 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 31320349d..8d4a6f91c 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 @@ -56,7 +56,7 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll Fabricate(:status, reblog_of_id: status.id) end - it 'returns http unautharized' do + it 'returns http unauthorized' do get :index, params: { status_id: status.id } expect(response).to have_http_status(404) end diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb index 190dfad11..2eb30af74 100644 --- a/spec/controllers/api/v1/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/statuses_controller_spec.rb @@ -130,7 +130,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do let(:status) { Fabricate(:status, account: user.account, visibility: :private) } describe 'GET #show' do - it 'returns http unautharized' do + it 'returns http unauthorized' do get :show, params: { id: status.id } expect(response).to have_http_status(404) end @@ -141,7 +141,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do Fabricate(:status, account: user.account, thread: status) end - it 'returns http unautharized' do + it 'returns http unauthorized' do get :context, params: { id: status.id } expect(response).to have_http_status(404) end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 0fb4ddbcf..53e163d49 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -187,30 +187,30 @@ describe ApplicationController, type: :controller do controller do before_action :require_admin! - def sucesss + def success head 200 end end before do - routes.draw { get 'sucesss' => 'anonymous#sucesss' } + routes.draw { get 'success' => 'anonymous#success' } end it 'returns a 403 if current user is not admin' do sign_in(Fabricate(:user, admin: false)) - get 'sucesss' + get 'success' expect(response).to have_http_status(403) end it 'returns a 403 if current user is only a moderator' do sign_in(Fabricate(:user, moderator: true)) - get 'sucesss' + get 'success' expect(response).to have_http_status(403) end it 'does nothing if current user is admin' do sign_in(Fabricate(:user, admin: true)) - get 'sucesss' + get 'success' expect(response).to have_http_status(200) end end @@ -219,30 +219,30 @@ describe ApplicationController, type: :controller do controller do before_action :require_staff! - def sucesss + def success head 200 end end before do - routes.draw { get 'sucesss' => 'anonymous#sucesss' } + routes.draw { get 'success' => 'anonymous#success' } end it 'returns a 403 if current user is not admin or moderator' do sign_in(Fabricate(:user, admin: false, moderator: false)) - get 'sucesss' + get 'success' expect(response).to have_http_status(403) end it 'does nothing if current user is moderator' do sign_in(Fabricate(:user, moderator: true)) - get 'sucesss' + get 'success' expect(response).to have_http_status(200) end it 'does nothing if current user is admin' do sign_in(Fabricate(:user, admin: true)) - get 'sucesss' + get 'success' expect(response).to have_http_status(200) end end diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb index 9986efa51..05fae67fa 100644 --- a/spec/controllers/statuses_controller_spec.rb +++ b/spec/controllers/statuses_controller_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' describe StatusesController do render_views - shared_examples 'cachable response' do + shared_examples 'cacheable response' do it 'does not set cookies' do expect(response.cookies).to be_empty expect(response.headers['Set-Cookies']).to be nil @@ -108,7 +108,7 @@ describe StatusesController do expect(response.headers['Vary']).to eq 'Accept' end - it_behaves_like 'cachable response' + it_behaves_like 'cacheable response' it 'returns Content-Type header' do expect(response.headers['Content-Type']).to include 'application/activity+json' @@ -496,7 +496,7 @@ describe StatusesController do expect(response.headers['Vary']).to eq 'Accept' end - it_behaves_like 'cachable response' + it_behaves_like 'cacheable response' it 'returns Content-Type header' do expect(response.headers['Content-Type']).to include 'application/activity+json' diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index f09e32ecc..b9d38d8c6 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -60,7 +60,7 @@ describe ApplicationHelper do end describe 'favicon_path' do - it 'returns /favicon.ico on production enviromnent' do + it 'returns /favicon.ico on production environment' do expect(Rails.env).to receive(:production?).and_return(true) expect(helper.favicon_path).to eq '/favicon.ico' end diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb index 2230f9710..cd9fb936c 100644 --- a/spec/lib/tag_manager_spec.rb +++ b/spec/lib/tag_manager_spec.rb @@ -6,7 +6,7 @@ RSpec.describe TagManager do around do |example| original_local_domain = Rails.configuration.x.local_domain - Rails.configuration.x.local_domain = 'domain.test' + Rails.configuration.x.local_domain = 'domain.example.com' example.run @@ -18,11 +18,11 @@ RSpec.describe TagManager do end it 'returns true if the slash-stripped string equals to local domain' do - expect(TagManager.instance.local_domain?('DoMaIn.Test/')).to eq true + expect(TagManager.instance.local_domain?('DoMaIn.Example.com/')).to eq true end it 'returns false for irrelevant string' do - expect(TagManager.instance.local_domain?('DoMaIn.Test!')).to eq false + expect(TagManager.instance.local_domain?('DoMaIn.Example.com!')).to eq false end end @@ -31,7 +31,7 @@ RSpec.describe TagManager do around do |example| original_web_domain = Rails.configuration.x.web_domain - Rails.configuration.x.web_domain = 'domain.test' + Rails.configuration.x.web_domain = 'domain.example.com' example.run @@ -43,11 +43,11 @@ RSpec.describe TagManager do end it 'returns true if the slash-stripped string equals to web domain' do - expect(TagManager.instance.web_domain?('DoMaIn.Test/')).to eq true + expect(TagManager.instance.web_domain?('DoMaIn.Example.com/')).to eq true end it 'returns false for string with irrelevant characters' do - expect(TagManager.instance.web_domain?('DoMaIn.Test!')).to eq false + expect(TagManager.instance.web_domain?('DoMaIn.Example.com!')).to eq false end end @@ -57,7 +57,7 @@ RSpec.describe TagManager do end it 'returns normalized domain' do - expect(TagManager.instance.normalize_domain('DoMaIn.Test/')).to eq 'domain.test' + expect(TagManager.instance.normalize_domain('DoMaIn.Example.com/')).to eq 'domain.example.com' end end @@ -69,18 +69,18 @@ RSpec.describe TagManager do end it 'returns true if the normalized string with port is local URL' do - Rails.configuration.x.web_domain = 'domain.test:42' - expect(TagManager.instance.local_url?('https://DoMaIn.Test:42/')).to eq true + Rails.configuration.x.web_domain = 'domain.example.com:42' + expect(TagManager.instance.local_url?('https://DoMaIn.Example.com:42/')).to eq true end it 'returns true if the normalized string without port is local URL' do - Rails.configuration.x.web_domain = 'domain.test' - expect(TagManager.instance.local_url?('https://DoMaIn.Test/')).to eq true + Rails.configuration.x.web_domain = 'domain.example.com' + expect(TagManager.instance.local_url?('https://DoMaIn.Example.com/')).to eq true end it 'returns false for string with irrelevant characters' do - Rails.configuration.x.web_domain = 'domain.test' - expect(TagManager.instance.local_url?('https://domainn.test/')).to eq false + Rails.configuration.x.web_domain = 'domain.example.com' + expect(TagManager.instance.local_url?('https://domain.example.net/')).to eq false end end end diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index 3d29c0219..df32a7c9d 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -119,7 +119,7 @@ describe Report do end end - describe 'validatiions' do + describe 'validations' do it 'has a valid fabricator' do report = Fabricate(:report) report.valid? diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb index 1933ee014..731c041d1 100644 --- a/spec/policies/user_policy_spec.rb +++ b/spec/policies/user_policy_spec.rb @@ -114,13 +114,13 @@ RSpec.describe UserPolicy do permissions :promote? do context 'admin?' do - context 'promoteable?' do + context 'promotable?' do it 'permits' do expect(subject).to permit(admin, john.user) end end - context '!promoteable?' do + context '!promotable?' do it 'denies' do expect(subject).to_not permit(admin, admin.user) end diff --git a/spec/services/unsuspend_account_service_spec.rb b/spec/services/unsuspend_account_service_spec.rb index d52cb6cc0..0593beb6f 100644 --- a/spec/services/unsuspend_account_service_spec.rb +++ b/spec/services/unsuspend_account_service_spec.rb @@ -63,20 +63,20 @@ RSpec.describe UnsuspendAccountService, type: :service do describe 'unsuspending a remote account' do include_examples 'common behavior' do let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) } - let!(:reslove_account_service) { double } + let!(:resolve_account_service) { double } before do - allow(ResolveAccountService).to receive(:new).and_return(reslove_account_service) + allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service) end context 'when the account is not remotely suspended' do before do - allow(reslove_account_service).to receive(:call).with(account).and_return(account) + allow(resolve_account_service).to receive(:call).with(account).and_return(account) end it 're-fetches the account' do subject.call - expect(reslove_account_service).to have_received(:call).with(account) + expect(resolve_account_service).to have_received(:call).with(account) end it "merges back into local followers' feeds" do @@ -92,7 +92,7 @@ RSpec.describe UnsuspendAccountService, type: :service do context 'when the account is remotely suspended' do before do - allow(reslove_account_service).to receive(:call).with(account) do |account| + allow(resolve_account_service).to receive(:call).with(account) do |account| account.suspend!(origin: :remote) account end @@ -100,7 +100,7 @@ RSpec.describe UnsuspendAccountService, type: :service do it 're-fetches the account' do subject.call - expect(reslove_account_service).to have_received(:call).with(account) + expect(resolve_account_service).to have_received(:call).with(account) end it "does not merge back into local followers' feeds" do @@ -116,12 +116,12 @@ RSpec.describe UnsuspendAccountService, type: :service do context 'when the account is remotely deleted' do before do - allow(reslove_account_service).to receive(:call).with(account).and_return(nil) + allow(resolve_account_service).to receive(:call).with(account).and_return(nil) end it 're-fetches the account' do subject.call - expect(reslove_account_service).to have_received(:call).with(account) + expect(resolve_account_service).to have_received(:call).with(account) end it "does not merge back into local followers' feeds" do diff --git a/spec/support/stories/profile_stories.rb b/spec/support/stories/profile_stories.rb index 75b413330..0c4a14d1c 100644 --- a/spec/support/stories/profile_stories.rb +++ b/spec/support/stories/profile_stories.rb @@ -22,7 +22,7 @@ module ProfileStories def with_alice_as_local_user @alice_bio = '@alice and @bob are fictional characters commonly used as'\ 'placeholder names in #cryptology, as well as #science and'\ - 'engineering 📖 literature. Not affilated with @pepe.' + 'engineering 📖 literature. Not affiliated with @pepe.' @alice = Fabricate( :user, -- cgit From edf09ec747ebba5a170e27eb13663462a116ec6c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 7 Mar 2022 09:36:47 +0100 Subject: Add `/api/v1/accounts/familiar_followers` to REST API (#17700) * Add `/api/v1/accounts/familiar_followers` to REST API * Change hide network preference to be stored consistently for local and remote accounts * Add dummy classes to migration * Apply suggestions from code review Co-authored-by: Claire Co-authored-by: Claire --- .../v1/accounts/familiar_followers_controller.rb | 25 ++++++++++ app/controllers/follower_accounts_controller.rb | 6 +-- app/controllers/following_accounts_controller.rb | 6 +-- app/controllers/settings/preferences_controller.rb | 1 - app/controllers/settings/profiles_controller.rb | 2 +- app/lib/user_settings_decorator.rb | 5 -- app/models/account.rb | 4 +- app/models/user.rb | 6 +-- app/presenters/familiar_followers_presenter.rb | 17 +++++++ .../rest/familiar_followers_serializer.rb | 11 ++++ app/views/follower_accounts/index.html.haml | 2 +- app/views/following_accounts/index.html.haml | 2 +- .../settings/preferences/other/show.html.haml | 3 -- app/views/settings/profiles/show.html.haml | 5 +- config/locales/simple_form.en.yml | 3 +- config/routes.rb | 1 + config/settings.yml | 1 - ...220304195405_migrate_hide_network_preference.rb | 37 ++++++++++++++ db/schema.rb | 2 +- .../follower_accounts_controller_spec.rb | 2 +- .../following_accounts_controller_spec.rb | 2 +- .../familiar_followers_presenter_spec.rb | 58 ++++++++++++++++++++++ 22 files changed, 169 insertions(+), 32 deletions(-) create mode 100644 app/controllers/api/v1/accounts/familiar_followers_controller.rb create mode 100644 app/presenters/familiar_followers_presenter.rb create mode 100644 app/serializers/rest/familiar_followers_serializer.rb create mode 100644 db/migrate/20220304195405_migrate_hide_network_preference.rb create mode 100644 spec/presenters/familiar_followers_presenter_spec.rb (limited to 'spec/controllers') diff --git a/app/controllers/api/v1/accounts/familiar_followers_controller.rb b/app/controllers/api/v1/accounts/familiar_followers_controller.rb new file mode 100644 index 000000000..b0bd8018a --- /dev/null +++ b/app/controllers/api/v1/accounts/familiar_followers_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::FamiliarFollowersController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:follows' } + before_action :require_user! + before_action :set_accounts + + def index + render json: familiar_followers.accounts, each_serializer: REST::FamiliarFollowersSerializer + end + + private + + def set_accounts + @accounts = Account.without_suspended.where(id: account_ids).select('id, hide_collections').index_by(&:id).values_at(*account_ids).compact + end + + def familiar_followers + FamiliarFollowersPresenter.new(@accounts, current_user.account_id) + end + + def account_ids + Array(params[:id]).map(&:to_i) + end +end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index b3589a39f..f3f8336c9 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -15,13 +15,13 @@ class FollowerAccountsController < ApplicationController format.html do expires_in 0, public: true unless user_signed_in? - next if @account.user_hides_network? + next if @account.hide_collections? follows end format.json do - raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network? + raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections? expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?) @@ -82,7 +82,7 @@ class FollowerAccountsController < ApplicationController end def restrict_fields_to - if page_requested? || !@account.user_hides_network? + if page_requested? || !@account.hide_collections? # Return all fields else %i(id type total_items) diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 8a72dc475..9d7f4c9bf 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -15,13 +15,13 @@ class FollowingAccountsController < ApplicationController format.html do expires_in 0, public: true unless user_signed_in? - next if @account.user_hides_network? + next if @account.hide_collections? follows end format.json do - raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network? + raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections? expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?) @@ -82,7 +82,7 @@ class FollowingAccountsController < ApplicationController end def restrict_fields_to - if page_requested? || !@account.user_hides_network? + if page_requested? || !@account.hide_collections? # Return all fields else %i(id type total_items) diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 32b5d7948..c7492700c 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -47,7 +47,6 @@ class Settings::PreferencesController < Settings::BaseController :setting_system_font_ui, :setting_noindex, :setting_theme, - :setting_hide_network, :setting_aggregate_reblogs, :setting_show_application, :setting_advanced_layout, diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 0c15447a6..be5b4f302 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -20,7 +20,7 @@ class Settings::ProfilesController < Settings::BaseController private def account_params - params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value]) + params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, :hide_collections, fields_attributes: [:name, :value]) end def set_account diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index e37bc6d9f..de054e403 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -31,7 +31,6 @@ class UserSettingsDecorator user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') user.settings['noindex'] = noindex_preference if change?('setting_noindex') user.settings['theme'] = theme_preference if change?('setting_theme') - user.settings['hide_network'] = hide_network_preference if change?('setting_hide_network') user.settings['aggregate_reblogs'] = aggregate_reblogs_preference if change?('setting_aggregate_reblogs') user.settings['show_application'] = show_application_preference if change?('setting_show_application') user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout') @@ -97,10 +96,6 @@ class UserSettingsDecorator boolean_cast_setting 'setting_noindex' end - def hide_network_preference - boolean_cast_setting 'setting_hide_network' - end - def show_application_preference boolean_cast_setting 'setting_show_application' end diff --git a/app/models/account.rb b/app/models/account.rb index dfdf9045f..1717f1605 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -349,11 +349,11 @@ class Account < ApplicationRecord end def hides_followers? - hide_collections? || user_hides_network? + hide_collections? end def hides_following? - hide_collections? || user_hides_network? + hide_collections? end def object_type diff --git a/app/models/user.rb b/app/models/user.rb index bbf850d84..146bdcd2a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -126,7 +126,7 @@ class User < ApplicationRecord has_many :session_activations, dependent: :destroy delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal, - :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network, + :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images, :disable_swiping, @@ -273,10 +273,6 @@ class User < ApplicationRecord settings.notification_emails['trending_tag'] end - def hides_network? - @hides_network ||= settings.hide_network - end - def aggregates_reblogs? @aggregates_reblogs ||= settings.aggregate_reblogs end diff --git a/app/presenters/familiar_followers_presenter.rb b/app/presenters/familiar_followers_presenter.rb new file mode 100644 index 000000000..c1d944b80 --- /dev/null +++ b/app/presenters/familiar_followers_presenter.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class FamiliarFollowersPresenter + class Result < ActiveModelSerializers::Model + attributes :id, :accounts + end + + def initialize(accounts, current_account_id) + @accounts = accounts + @current_account_id = current_account_id + end + + def accounts + map = Follow.includes(account: :account_stat).where(target_account_id: @accounts.map(&:id)).where(account_id: Follow.where(account_id: @current_account_id).joins(:target_account).merge(Account.where(hide_collections: [nil, false])).select(:target_account_id)).group_by(&:target_account_id) + @accounts.map { |account| Result.new(id: account.id, accounts: (account.hide_collections? ? [] : (map[account.id] || [])).map(&:account)) } + end +end diff --git a/app/serializers/rest/familiar_followers_serializer.rb b/app/serializers/rest/familiar_followers_serializer.rb new file mode 100644 index 000000000..0a7e923f8 --- /dev/null +++ b/app/serializers/rest/familiar_followers_serializer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class REST::FamiliarFollowersSerializer < ActiveModel::Serializer + attribute :id + + has_many :accounts, serializer: REST::AccountSerializer + + def id + object.id.to_s + end +end diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml index 645dd2de1..92de35a9f 100644 --- a/app/views/follower_accounts/index.html.haml +++ b/app/views/follower_accounts/index.html.haml @@ -7,7 +7,7 @@ = render 'accounts/header', account: @account -- if @account.user_hides_network? +- if @account.hide_collections? .nothing-here= t('accounts.network_hidden') - elsif user_signed_in? && @account.blocking?(current_account) .nothing-here= t('accounts.unavailable') diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml index 17fe79018..9bb1a9edd 100644 --- a/app/views/following_accounts/index.html.haml +++ b/app/views/following_accounts/index.html.haml @@ -7,7 +7,7 @@ = render 'accounts/header', account: @account -- if @account.user_hides_network? +- if @account.hide_collections? .nothing-here= t('accounts.network_hidden') - elsif user_signed_in? && @account.blocking?(current_account) .nothing-here= t('accounts.unavailable') diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index b7ae3d2ef..44f4af2eb 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -10,9 +10,6 @@ .fields-group = f.input :setting_noindex, as: :boolean, wrapper: :with_label - .fields-group - = f.input :setting_hide_network, as: :boolean, wrapper: :with_label - .fields-group = f.input :setting_aggregate_reblogs, as: :boolean, wrapper: :with_label, recommended: true diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index d325a9ea5..fe9666d84 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -30,7 +30,10 @@ = f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot') .fields-group - = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t(Setting.profile_directory ? 'simple_form.hints.defaults.discoverable' : 'simple_form.hints.defaults.discoverable_no_directory'), recommended: true + = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable'), recommended: true + + .fields-group + = f.input :hide_collections, as: :boolean, wrapper: :with_label, label: t('simple_form.labels.defaults.setting_hide_network'), hint: t('simple_form.hints.defaults.setting_hide_network') %hr.spacer/ diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index c5e75b408..b19b7891f 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -37,8 +37,7 @@ en: current_password: For security purposes please enter the password of the current account current_username: To confirm, please enter the username of the current account digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence - discoverable: Allow your account to be discovered by strangers through recommendations, profile directory and other features - discoverable_no_directory: Allow your account to be discovered by strangers through recommendations and other features + discoverable: Allow your account to be discovered by strangers through recommendations, trends and other features email: You will be sent a confirmation e-mail fields: You can have up to 4 items displayed as a table on your profile header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px diff --git a/config/routes.rb b/config/routes.rb index 25eb1558f..9e2f7a648 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -493,6 +493,7 @@ Rails.application.routes.draw do resource :search, only: :show, controller: :search resource :lookup, only: :show, controller: :lookup resources :relationships, only: :index + resources :familiar_followers, only: :index end resources :accounts, only: [:create, :show] do diff --git a/config/settings.yml b/config/settings.yml index e63788ba2..06dd2b3f3 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -17,7 +17,6 @@ defaults: &defaults timeline_preview: true show_staff_badge: true default_sensitive: false - hide_network: false unfollow_modal: false boost_modal: false delete_modal: true diff --git a/db/migrate/20220304195405_migrate_hide_network_preference.rb b/db/migrate/20220304195405_migrate_hide_network_preference.rb new file mode 100644 index 000000000..102ee46d6 --- /dev/null +++ b/db/migrate/20220304195405_migrate_hide_network_preference.rb @@ -0,0 +1,37 @@ +class MigrateHideNetworkPreference < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + # Dummy classes, to make migration possible across version changes + class Account < ApplicationRecord + has_one :user, inverse_of: :account + scope :local, -> { where(domain: nil) } + end + + class User < ApplicationRecord + belongs_to :account + end + + def up + Account.reset_column_information + + Setting.unscoped.where(thing_type: 'User', var: 'hide_network').find_each do |setting| + account = User.find(setting.thing_id).account + + ApplicationRecord.transaction do + account.update(hide_collections: setting.value) + setting.delete + end + rescue ActiveRecord::RecordNotFound + next + end + end + + def down + Account.local.where(hide_collections: true).includes(:user).find_each do |account| + ApplicationRecord.transaction do + Setting.create(thing_type: 'User', thing_id: account.user.id, var: 'hide_network', value: account.hide_collections?) + account.update(hide_collections: nil) + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 756e5e9ab..3666804ee 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: 2022_02_27_041951) do +ActiveRecord::Schema.define(version: 2022_03_04_195405) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/spec/controllers/follower_accounts_controller_spec.rb b/spec/controllers/follower_accounts_controller_spec.rb index eb095cf30..4d2a6e01a 100644 --- a/spec/controllers/follower_accounts_controller_spec.rb +++ b/spec/controllers/follower_accounts_controller_spec.rb @@ -103,7 +103,7 @@ describe FollowerAccountsController do context 'when account hides their network' do before do - alice.user.settings.hide_network = true + alice.update(hide_collections: true) end it 'returns followers count' do diff --git a/spec/controllers/following_accounts_controller_spec.rb b/spec/controllers/following_accounts_controller_spec.rb index af5ce0787..bb6d221ca 100644 --- a/spec/controllers/following_accounts_controller_spec.rb +++ b/spec/controllers/following_accounts_controller_spec.rb @@ -103,7 +103,7 @@ describe FollowingAccountsController do context 'when account hides their network' do before do - alice.user.settings.hide_network = true + alice.update(hide_collections: true) end it 'returns followers count' do diff --git a/spec/presenters/familiar_followers_presenter_spec.rb b/spec/presenters/familiar_followers_presenter_spec.rb new file mode 100644 index 000000000..17be4b971 --- /dev/null +++ b/spec/presenters/familiar_followers_presenter_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe FamiliarFollowersPresenter do + describe '#accounts' do + let(:account) { Fabricate(:account) } + let(:familiar_follower) { Fabricate(:account) } + let(:requested_accounts) { Fabricate.times(2, :account) } + + subject { described_class.new(requested_accounts, account.id) } + + before do + familiar_follower.follow!(requested_accounts.first) + account.follow!(familiar_follower) + end + + it 'returns a result for each requested account' do + expect(subject.accounts.map(&:id)).to eq requested_accounts.map(&:id) + end + + it 'returns followers you follow' do + result = subject.accounts.first + + expect(result).to_not be_nil + expect(result.id).to eq requested_accounts.first.id + expect(result.accounts).to match_array([familiar_follower]) + end + + context 'when requested account hides followers' do + before do + requested_accounts.first.update(hide_collections: true) + end + + it 'does not return followers you follow' do + result = subject.accounts.first + + expect(result).to_not be_nil + expect(result.id).to eq requested_accounts.first.id + expect(result.accounts).to be_empty + end + end + + context 'when familiar follower hides follows' do + before do + familiar_follower.update(hide_collections: true) + end + + it 'does not return followers you follow' do + result = subject.accounts.first + + expect(result).to_not be_nil + expect(result.id).to eq requested_accounts.first.id + expect(result.accounts).to be_empty + end + end + end +end -- cgit