From e445a8af64908b2bdb721bec74c113e8258a129b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 6 Sep 2019 13:55:51 +0200 Subject: Add timeline read markers API (#11762) Fix #4093 --- app/controllers/api/v1/markers_controller.rb | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 app/controllers/api/v1/markers_controller.rb (limited to 'app/controllers') diff --git a/app/controllers/api/v1/markers_controller.rb b/app/controllers/api/v1/markers_controller.rb new file mode 100644 index 000000000..28c2ec791 --- /dev/null +++ b/app/controllers/api/v1/markers_controller.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class Api::V1::MarkersController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:index] + before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, except: [:index] + + before_action :require_user! + + def index + @markers = current_user.markers.where(timeline: Array(params[:timeline])).each_with_object({}) { |marker, h| h[marker.timeline] = marker } + render json: serialize_map(@markers) + end + + def create + Marker.transaction do + @markers = {} + + resource_params.each_pair do |timeline, timeline_params| + @markers[timeline] = current_user.markers.find_or_initialize_by(timeline: timeline) + @markers[timeline].update!(timeline_params) + end + end + + render json: serialize_map(@markers) + rescue ActiveRecord::StaleObjectError + render json: { error: 'Conflict during update, please try again' }, status: 409 + end + + private + + def serialize_map(map) + serialized = {} + + map.each_pair do |key, value| + serialized[key] = ActiveModelSerializers::SerializableResource.new(value, serializer: REST::MarkerSerializer).as_json + end + + Oj.dump(serialized) + end + + def resource_params + params.slice(*Marker::TIMELINES).permit(*Marker::TIMELINES.map { |timeline| { timeline.to_sym => [:last_read_id] } }) + end +end -- cgit From d7268befa853ac4a99f9d066c38330d4fc0bfc31 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Sat, 7 Sep 2019 09:47:51 +0900 Subject: Add healthcheck endpoint for web (#11770) --- Gemfile | 1 + Gemfile.lock | 3 +++ app/controllers/application_controller.rb | 2 +- config/initializers/health_check.rb | 6 ++++++ config/routes.rb | 2 ++ docker-compose.yml | 4 ++-- 6 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 config/initializers/health_check.rb (limited to 'app/controllers') diff --git a/Gemfile b/Gemfile index 1cbf23cf2..d9740702f 100644 --- a/Gemfile +++ b/Gemfile @@ -50,6 +50,7 @@ gem 'fastimage' gem 'goldfinger', '~> 2.1' gem 'hiredis', '~> 0.6' gem 'redis-namespace', '~> 1.5' +gem 'health_check', '~> 3.0' gem 'htmlentities', '~> 4.3' gem 'http', '~> 3.3' gem 'http_accept_language', '~> 2.1' diff --git a/Gemfile.lock b/Gemfile.lock index 8ecc8ebb7..b0f6bf083 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -278,6 +278,8 @@ GEM concurrent-ruby (~> 1.0) hashdiff (1.0.0) hashie (3.6.0) + health_check (3.0.0) + railties (>= 5.0) heapy (0.1.4) highline (2.0.1) hiredis (0.6.3) @@ -706,6 +708,7 @@ DEPENDENCIES fuubar (~> 2.4) goldfinger (~> 2.1) hamlit-rails (~> 0.2) + health_check (~> 3.0) hiredis (~> 0.6) htmlentities (~> 4.3) http (~> 3.3) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5b343a276..bd3d13774 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -41,7 +41,7 @@ class ApplicationController < ActionController::Base private def https_enabled? - Rails.env.production? + Rails.env.production? && !request.path.start_with?('/health') end def authorized_fetch_mode? diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb new file mode 100644 index 000000000..eece67b10 --- /dev/null +++ b/config/initializers/health_check.rb @@ -0,0 +1,6 @@ +HealthCheck.setup do |config| + config.uri = 'health' + + config.standard_checks = %w(database migrations cache) + config.full_checks = %w(database migrations cache) +end diff --git a/config/routes.rb b/config/routes.rb index 74a162f32..fe8425341 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,6 +10,8 @@ Rails.application.routes.draw do mount LetterOpenerWeb::Engine, at: 'letter_opener' if Rails.env.development? + health_check_routes + authenticate :user, lambda { |u| u.admin? } do mount Sidekiq::Web, at: 'sidekiq', as: :sidekiq mount PgHero::Engine, at: 'pghero', as: :pghero diff --git a/docker-compose.yml b/docker-compose.yml index 740684966..20649e424 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,7 @@ services: - external_network - internal_network healthcheck: - test: ["CMD-SHELL", "wget -q --spider --header 'x-forwarded-proto: https' --proxy=off localhost:3000/api/v1/instance || exit 1"] + test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:3000/health || exit 1"] ports: - "127.0.0.1:3000:3000" depends_on: @@ -63,7 +63,7 @@ services: - external_network - internal_network healthcheck: - test: ["CMD-SHELL", "wget -q --spider --header 'x-forwarded-proto: https' --proxy=off localhost:4000/api/v1/streaming/health || exit 1"] + test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1"] ports: - "127.0.0.1:4000:4000" depends_on: -- cgit From 9c9dcf580ab9b40c3fd420159a0a02ea4dd11925 Mon Sep 17 00:00:00 2001 From: Takeshi Umeda Date: Mon, 9 Sep 2019 17:50:33 +0900 Subject: Add featured tags API (#11778) * Add featured tags API * Remove show and update, change scope, fix code style --- .../api/v1/featured_tags/suggestions_controller.rb | 20 +++++++++++ app/controllers/api/v1/featured_tags_controller.rb | 40 ++++++++++++++++++++++ app/serializers/rest/featured_tag_serializer.rb | 9 +++++ config/routes.rb | 6 ++++ 4 files changed, 75 insertions(+) create mode 100644 app/controllers/api/v1/featured_tags/suggestions_controller.rb create mode 100644 app/controllers/api/v1/featured_tags_controller.rb create mode 100644 app/serializers/rest/featured_tag_serializer.rb (limited to 'app/controllers') diff --git a/app/controllers/api/v1/featured_tags/suggestions_controller.rb b/app/controllers/api/v1/featured_tags/suggestions_controller.rb new file mode 100644 index 000000000..fb27ef88b --- /dev/null +++ b/app/controllers/api/v1/featured_tags/suggestions_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index + + before_action :require_user! + before_action :set_most_used_tags, only: :index + + respond_to :json + + def index + render json: @most_used_tags, each_serializer: REST::TagSerializer + end + + private + + def set_most_used_tags + @most_used_tags = Tag.most_used(current_account).where.not(id: current_account.featured_tags).limit(10) + end +end diff --git a/app/controllers/api/v1/featured_tags_controller.rb b/app/controllers/api/v1/featured_tags_controller.rb new file mode 100644 index 000000000..e4e836c97 --- /dev/null +++ b/app/controllers/api/v1/featured_tags_controller.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class Api::V1::FeaturedTagsController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index + + before_action :require_user! + before_action :set_featured_tags, only: :index + before_action :set_featured_tag, except: [:index, :create] + + def index + render json: @featured_tags, each_serializer: REST::FeaturedTagSerializer + end + + def create + @featured_tag = current_account.featured_tags.new(featured_tag_params) + @featured_tag.reset_data + @featured_tag.save! + render json: @featured_tag, serializer: REST::FeaturedTagSerializer + end + + def destroy + @featured_tag.destroy! + render_empty + end + + private + + def set_featured_tag + @featured_tag = current_account.featured_tags.find(params[:id]) + end + + def set_featured_tags + @featured_tags = current_account.featured_tags.order(statuses_count: :desc) + end + + def featured_tag_params + params.permit(:name) + end +end diff --git a/app/serializers/rest/featured_tag_serializer.rb b/app/serializers/rest/featured_tag_serializer.rb new file mode 100644 index 000000000..08121ff16 --- /dev/null +++ b/app/serializers/rest/featured_tag_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class REST::FeaturedTagSerializer < ActiveModel::Serializer + attributes :id, :name, :statuses_count, :last_status_at + + def id + object.id.to_s + end +end diff --git a/config/routes.rb b/config/routes.rb index fe8425341..1ebf9e066 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -380,6 +380,12 @@ Rails.application.routes.draw do resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts' end + namespace :featured_tags do + get :suggestions, to: 'suggestions#index' + end + + resources :featured_tags, only: [:index, :create, :destroy] + resources :polls, only: [:create, :show] do resources :votes, only: :create, controller: 'polls/votes' end -- cgit From 261e52268c05d2da4459a23e2898555dd5db5771 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 9 Sep 2019 12:50:09 +0200 Subject: Add batch approve/reject for pending hashtags in admin UI (#11791) --- app/controllers/admin/tags_controller.rb | 41 +++++++++++++++++++++++++++--- app/javascript/styles/mastodon/tables.scss | 10 ++++++++ app/models/form/tag_batch.rb | 33 ++++++++++++++++++++++++ app/views/admin/tags/_tag.html.haml | 30 ++++++++++++---------- app/views/admin/tags/index.html.haml | 37 ++++++++++++++++++++++++++- config/locales/en.yml | 1 + config/routes.rb | 9 ++++++- 7 files changed, 142 insertions(+), 19 deletions(-) create mode 100644 app/models/form/tag_batch.rb (limited to 'app/controllers') diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index 8bd4e5f8b..376ebe44d 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -3,12 +3,33 @@ module Admin class TagsController < BaseController before_action :set_tags, only: :index - before_action :set_tag, except: :index - before_action :set_usage_by_domain, except: :index - before_action :set_counters, except: :index + before_action :set_tag, except: [:index, :batch, :approve_all, :reject_all] + before_action :set_usage_by_domain, except: [:index, :batch, :approve_all, :reject_all] + before_action :set_counters, except: [:index, :batch, :approve_all, :reject_all] def index authorize :tag, :index? + + @form = Form::TagBatch.new + end + + def batch + @form = Form::TagBatch.new(form_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') + ensure + redirect_to admin_tags_path(filter_params) + end + + def approve_all + Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'approve').save + redirect_to admin_tags_path(filter_params) + end + + def reject_all + Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'reject').save + redirect_to admin_tags_path(filter_params) end def show @@ -61,7 +82,7 @@ module Admin end def filter_params - params.slice(:context, :review).permit(:context, :review) + params.slice(:context, :review, :page).permit(:context, :review, :page) end def tag_params @@ -75,5 +96,17 @@ module Admin date.to_time(:utc).beginning_of_day.to_i end end + + def form_tag_batch_params + params.require(:form_tag_batch).permit(:action, tag_ids: []) + end + + def action_from_button + if params[:approve] + 'approve' + elsif params[:reject] + 'reject' + end + end end end diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index fe6beba5d..2aef099e6 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -211,6 +211,16 @@ a.table-action-link { padding: 0; } } + + .directory__tag { + margin: 0; + width: 100%; + + a { + background: transparent; + border-radius: 0; + } + } } .status__content { diff --git a/app/models/form/tag_batch.rb b/app/models/form/tag_batch.rb new file mode 100644 index 000000000..fd517a1a6 --- /dev/null +++ b/app/models/form/tag_batch.rb @@ -0,0 +1,33 @@ +# 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: Time.now.utc) + end + + def reject! + tags.each { |tag| authorize(tag, :update?) } + tags.update_all(trendable: false, reviewed_at: Time.now.utc) + end +end diff --git a/app/views/admin/tags/_tag.html.haml b/app/views/admin/tags/_tag.html.haml index 91af8e492..670f3bc05 100644 --- a/app/views/admin/tags/_tag.html.haml +++ b/app/views/admin/tags/_tag.html.haml @@ -1,16 +1,20 @@ -.directory__tag - = link_to admin_tag_path(tag.id) do - %h4 - = fa_icon 'hashtag' - = tag.name +.batch-table__row + %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox + = f.check_box :tag_ids, { multiple: true, include_hidden: false }, tag.id - %small - = t('admin.tags.in_directory', count: tag.accounts_count) - • - = t('admin.tags.unique_uses_today', count: tag.history.first[:accounts]) + .directory__tag + = link_to admin_tag_path(tag.id) do + %h4 + = fa_icon 'hashtag' + = tag.name - - if tag.trending? - = fa_icon 'fire fw' - = t('admin.tags.trending_right_now') + %small + = t('admin.tags.in_directory', count: tag.accounts_count) + • + = t('admin.tags.unique_uses_today', count: tag.history.first[:accounts]) - .trends__item__current= number_to_human tag.history.first[:uses], strip_insignificant_zeros: true + - if tag.trending? + = fa_icon 'fire fw' + = t('admin.tags.trending_right_now') + + .trends__item__current= number_to_human tag.history.first[:uses], strip_insignificant_zeros: true diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml index d994955ef..324d13d3e 100644 --- a/app/views/admin/tags/index.html.haml +++ b/app/views/admin/tags/index.html.haml @@ -1,6 +1,9 @@ - content_for :page_title do = t('admin.tags.title') +- content_for :header_tags do + = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous' + .filters .filter-subset %strong= t('admin.tags.context') @@ -18,5 +21,37 @@ %hr.spacer/ -= render @tags += form_for(@form, url: batch_admin_tags_path) do |f| + = hidden_field_tag :page, params[:page] || 1 + = hidden_field_tag :context, params[:context] + = hidden_field_tag :review, params[:review] + + .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 + - if params[:review] == 'pending_review' + = f.button safe_join([fa_icon('check'), t('admin.accounts.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + = f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + - else + %span.neutral-hint= t('generic.no_batch_actions_available') + + .batch-table__body + - if @tags.empty? + = nothing_here 'nothing-here--under-tabs' + - else + = render partial: 'tag', collection: @tags, locals: { f: f } + = paginate @tags + +- if params[:review] == 'pending_review' + %hr.spacer/ + + %div{ style: 'overflow: hidden' } + %div{ style: 'float: right' } + = link_to t('admin.accounts.reject_all'), reject_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' + + %div + = link_to t('admin.accounts.approve_all'), approve_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' diff --git a/config/locales/en.yml b/config/locales/en.yml index 687f5f2a0..42d8e0eb8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -727,6 +727,7 @@ en: all: All changes_saved_msg: Changes successfully saved! copy: Copy + no_batch_actions_available: No batch actions available on this page order_by: Order by save_changes: Save changes validation_errors: diff --git a/config/routes.rb b/config/routes.rb index 1ebf9e066..534e68814 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -251,7 +251,14 @@ Rails.application.routes.draw do end resources :account_moderation_notes, only: [:create, :destroy] - resources :tags, only: [:index, :show, :update] + + resources :tags, only: [:index, :show, :update] do + collection do + post :approve_all + post :reject_all + post :batch + end + end end get '/admin', to: redirect('/admin/dashboard', status: 302) -- cgit From 1110ea1a9162d5488e1ed5dbccd0803618e713f8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 9 Sep 2019 22:44:17 +0200 Subject: Add batch actions and categories to admin UI for custom emojis (#11793) --- app/controllers/admin/custom_emojis_controller.rb | 102 +++++++------------- app/javascript/styles/mastodon/tables.scss | 41 ++++++++ app/models/custom_emoji.rb | 6 ++ app/models/custom_emoji_category.rb | 2 + app/models/custom_emoji_filter.rb | 8 +- app/models/form/custom_emoji_batch.rb | 106 +++++++++++++++++++++ .../admin/custom_emojis/_custom_emoji.html.haml | 55 ++++++----- app/views/admin/custom_emojis/index.html.haml | 66 ++++++++++--- config/locales/en.yml | 3 + config/routes.rb | 8 +- .../admin/custom_emojis_controller_spec.rb | 60 ------------ 11 files changed, 281 insertions(+), 176 deletions(-) create mode 100644 app/models/form/custom_emoji_batch.rb (limited to 'app/controllers') diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index f77699166..2af90f051 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -2,19 +2,20 @@ module Admin class CustomEmojisController < BaseController - before_action :set_custom_emoji, except: [:index, :new, :create] - before_action :set_filter_params - include ObfuscateFilename + obfuscate_filename [:custom_emoji, :image] def index authorize :custom_emoji, :index? + @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) + @form = Form::CustomEmojiBatch.new end def new authorize :custom_emoji, :create? + @custom_emoji = CustomEmoji.new end @@ -31,69 +32,17 @@ module Admin end end - def update - authorize @custom_emoji, :update? - - if @custom_emoji.update(resource_params) - log_action :update, @custom_emoji - flash[:notice] = I18n.t('admin.custom_emojis.updated_msg') - else - flash[:alert] = I18n.t('admin.custom_emojis.update_failed_msg') - end - redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params) - end - - def destroy - authorize @custom_emoji, :destroy? - @custom_emoji.destroy! - log_action :destroy, @custom_emoji - flash[:notice] = I18n.t('admin.custom_emojis.destroyed_msg') - redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params) - end - - def copy - authorize @custom_emoji, :copy? - - emoji = CustomEmoji.find_or_initialize_by(domain: nil, - shortcode: @custom_emoji.shortcode) - emoji.image = @custom_emoji.image - - if emoji.save - log_action :create, emoji - flash[:notice] = I18n.t('admin.custom_emojis.copied_msg') - else - flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg') - end - - redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params) - end - - def enable - authorize @custom_emoji, :enable? - @custom_emoji.update!(disabled: false) - log_action :enable, @custom_emoji - flash[:notice] = I18n.t('admin.custom_emojis.enabled_msg') - redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params) - end - - def disable - authorize @custom_emoji, :disable? - @custom_emoji.update!(disabled: true) - log_action :disable, @custom_emoji - flash[:notice] = I18n.t('admin.custom_emojis.disabled_msg') - redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params) + def batch + @form = Form::CustomEmojiBatch.new(form_custom_emoji_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_custom_emojis_path(filter_params) end private - def set_custom_emoji - @custom_emoji = CustomEmoji.find(params[:id]) - end - - def set_filter_params - @filter_params = filter_params.to_hash.symbolize_keys - end - def resource_params params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker) end @@ -103,12 +52,29 @@ module Admin end def filter_params - params.permit( - :local, - :remote, - :by_domain, - :shortcode - ) + params.slice(:local, :remote, :by_domain, :shortcode, :page).permit(:local, :remote, :by_domain, :shortcode, :page) + end + + def action_from_button + if params[:update] + 'update' + elsif params[:list] + 'list' + elsif params[:unlist] + 'unlist' + elsif params[:enable] + 'enable' + elsif params[:disable] + 'disable' + elsif params[:copy] + 'copy' + elsif params[:delete] + 'delete' + end + end + + def form_custom_emoji_batch_params + params.require(:form_custom_emoji_batch).permit(:action, :category_id, :category_name, custom_emoji_ids: []) end end end diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 2aef099e6..d6403986f 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -180,6 +180,18 @@ a.table-action-link { } } + &__form { + padding: 16px; + border: 1px solid darken($ui-base-color, 8%); + border-top: 0; + background: $ui-base-color; + + .fields-row { + padding-top: 0; + margin-bottom: 0; + } + } + &__row { border: 1px solid darken($ui-base-color, 8%); border-top: 0; @@ -210,6 +222,35 @@ a.table-action-link { &--unpadded { padding: 0; } + + &--with-image { + display: flex; + align-items: center; + } + + &__image { + flex: 0 0 auto; + display: flex; + justify-content: center; + align-items: center; + margin-right: 10px; + + .emojione { + width: 32px; + height: 32px; + } + } + + &__text { + flex: 1 1 auto; + } + + &__extra { + flex: 0 0 auto; + text-align: right; + color: $darker-text-color; + font-weight: 500; + } } .directory__tag { diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index b21ad9042..0a4201a14 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -59,6 +59,12 @@ class CustomEmoji < ApplicationRecord :emoji end + def copy! + copy = self.class.find_or_initialize_by(domain: nil, shortcode: shortcode) + copy.image = image + copy.save! + end + class << self def from_text(text, domain) return [] if text.blank? diff --git a/app/models/custom_emoji_category.rb b/app/models/custom_emoji_category.rb index 7d8c0ee2d..3c87f2b2e 100644 --- a/app/models/custom_emoji_category.rb +++ b/app/models/custom_emoji_category.rb @@ -12,4 +12,6 @@ class CustomEmojiCategory < ApplicationRecord has_many :emojis, class_name: 'CustomEmoji', foreign_key: 'category_id', inverse_of: :category + + validates :name, presence: true, uniqueness: true end diff --git a/app/models/custom_emoji_filter.rb b/app/models/custom_emoji_filter.rb index 7649055d2..15b8da1d1 100644 --- a/app/models/custom_emoji_filter.rb +++ b/app/models/custom_emoji_filter.rb @@ -11,6 +11,8 @@ class CustomEmojiFilter scope = CustomEmoji.alphabetic params.each do |key, value| + next if key.to_s == 'page' + scope.merge!(scope_for(key, value)) if value.present? end @@ -22,13 +24,13 @@ class CustomEmojiFilter def scope_for(key, value) case key.to_s when 'local' - CustomEmoji.local + CustomEmoji.local.left_joins(:category).reorder(Arel.sql('custom_emoji_categories.name ASC NULLS FIRST, custom_emojis.shortcode ASC')) when 'remote' CustomEmoji.remote when 'by_domain' - CustomEmoji.where(domain: value.downcase) + CustomEmoji.where(domain: value.strip.downcase) when 'shortcode' - CustomEmoji.search(value) + CustomEmoji.search(value.strip) else raise "Unknown filter: #{key}" end diff --git a/app/models/form/custom_emoji_batch.rb b/app/models/form/custom_emoji_batch.rb new file mode 100644 index 000000000..076e8c9e3 --- /dev/null +++ b/app/models/form/custom_emoji_batch.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +class Form::CustomEmojiBatch + include ActiveModel::Model + include Authorization + include AccountableConcern + + attr_accessor :custom_emoji_ids, :action, :current_account, + :category_id, :category_name, :visible_in_picker + + def save + case action + when 'update' + update! + when 'list' + list! + when 'unlist' + unlist! + when 'enable' + enable! + when 'disable' + disable! + when 'copy' + copy! + when 'delete' + delete! + end + end + + private + + def custom_emojis + CustomEmoji.where(id: custom_emoji_ids) + end + + def update! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :update?) } + + category = begin + if category_id.present? + CustomEmojiCategory.find(category_id) + elsif category_name.present? + CustomEmojiCategory.create!(name: category_name) + end + end + + custom_emojis.each do |custom_emoji| + custom_emoji.update(category_id: category&.id) + log_action :update, custom_emoji + end + end + + def list! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :update?) } + + custom_emojis.each do |custom_emoji| + custom_emoji.update(visible_in_picker: true) + log_action :update, custom_emoji + end + end + + def unlist! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :update?) } + + custom_emojis.each do |custom_emoji| + custom_emoji.update(visible_in_picker: false) + log_action :update, custom_emoji + end + end + + def enable! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :enable?) } + + custom_emojis.each do |custom_emoji| + custom_emoji.update(disabled: false) + log_action :enable, custom_emoji + end + end + + def disable! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :disable?) } + + custom_emojis.each do |custom_emoji| + custom_emoji.update(disabled: true) + log_action :disable, custom_emoji + end + end + + def copy! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :copy?) } + + custom_emojis.each do |custom_emoji| + copied_custom_emoji = custom_emoji.copy! + log_action :create, copied_custom_emoji + end + end + + def delete! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :destroy?) } + + custom_emojis.each do |custom_emoji| + custom_emoji.destroy + log_action :destroy, custom_emoji + end + end +end diff --git a/app/views/admin/custom_emojis/_custom_emoji.html.haml b/app/views/admin/custom_emojis/_custom_emoji.html.haml index fbaa9a174..9e06a3b42 100644 --- a/app/views/admin/custom_emojis/_custom_emoji.html.haml +++ b/app/views/admin/custom_emojis/_custom_emoji.html.haml @@ -1,28 +1,31 @@ -%tr - %td - = custom_emoji_tag(custom_emoji) - %td - %samp= ":#{custom_emoji.shortcode}:" - %td - - if custom_emoji.local? - = t('admin.accounts.location.local') - - else - = link_to custom_emoji.domain, admin_custom_emojis_path(by_domain: custom_emoji.domain) - %td - - if custom_emoji.local? - - if custom_emoji.visible_in_picker - = table_link_to 'eye', t('admin.custom_emojis.listed'), admin_custom_emoji_path(custom_emoji, custom_emoji: { visible_in_picker: false }, page: params[:page], **@filter_params), method: :patch +.batch-table__row + %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox + = 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) + + .batch-table__row__content__text + %samp= ":#{custom_emoji.shortcode}:" + + - if custom_emoji.local? + %span.account-role.bot= custom_emoji.category&.name || t('admin.custom_emojis.uncategorized') + + .batch-table__row__content__extra + - if custom_emoji.local? + = t('admin.accounts.location.local') - else - = table_link_to 'eye-slash', t('admin.custom_emojis.unlisted'), admin_custom_emoji_path(custom_emoji, custom_emoji: { visible_in_picker: true }, page: params[:page], **@filter_params), method: :patch - - else - - if custom_emoji.local_counterpart.present? - = link_to safe_join([custom_emoji_tag(custom_emoji.local_counterpart), t('admin.custom_emojis.overwrite')]), copy_admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :post, class: 'table-action-link' + = custom_emoji.domain + + %br/ + + - if custom_emoji.disabled? + = t('admin.custom_emojis.disabled') - else - = table_link_to 'copy', t('admin.custom_emojis.copy'), copy_admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :post - %td - - if custom_emoji.disabled? - = table_link_to 'power-off', t('admin.custom_emojis.enable'), enable_admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } - - else - = table_link_to 'power-off', t('admin.custom_emojis.disable'), disable_admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } - %td - = table_link_to 'times', t('admin.custom_emojis.delete'), admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } + = t('admin.custom_emojis.enabled') + - if custom_emoji.local? + • + - if custom_emoji.visible_in_picker? + = t('admin.custom_emojis.listed') + - else + = t('admin.custom_emojis.unlisted') diff --git a/app/views/admin/custom_emojis/index.html.haml b/app/views/admin/custom_emojis/index.html.haml index 3a119276c..7320ce1bb 100644 --- a/app/views/admin/custom_emojis/index.html.haml +++ b/app/views/admin/custom_emojis/index.html.haml @@ -1,6 +1,9 @@ - content_for :page_title do = t('admin.custom_emojis.title') +- content_for :header_tags do + = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous' + .filters .filter-subset %strong= t('admin.accounts.location.title') @@ -20,8 +23,7 @@ = form_tag admin_custom_emojis_url, method: 'GET', class: 'simple_form' do .fields-group - Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key| - - if params[key].present? - = hidden_field_tag key, params[key] + = hidden_field_tag key, params[key] if params[key].present? - %i(shortcode by_domain).each do |key| .input.string.optional @@ -31,18 +33,54 @@ %button= t('admin.accounts.search') = link_to t('admin.accounts.reset'), admin_custom_emojis_path, class: 'button negative' -.table-wrapper - %table.table - %thead - %tr - %th= t('admin.custom_emojis.emoji') - %th= t('admin.custom_emojis.shortcode') - %th= t('admin.accounts.domain') - %th - %th - %th - %tbody - = render @custom_emojis += form_for(@form, url: batch_admin_custom_emojis_path) do |f| + = hidden_field_tag :page, params[:page] || 1 + + - Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.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 + - if params[:local] == '1' + = f.button safe_join([fa_icon('save'), t('generic.save_changes')]), name: :update, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + = f.button safe_join([fa_icon('eye'), t('admin.custom_emojis.list')]), name: :list, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + = f.button safe_join([fa_icon('eye-slash'), t('admin.custom_emojis.unlist')]), name: :unlist, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + = f.button safe_join([fa_icon('power-off'), t('admin.custom_emojis.enable')]), name: :enable, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + = f.button safe_join([fa_icon('power-off'), t('admin.custom_emojis.disable')]), name: :disable, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + = f.button safe_join([fa_icon('times'), t('admin.custom_emojis.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + - unless params[:local] == '1' + = f.button safe_join([fa_icon('copy'), t('admin.custom_emojis.copy')]), name: :copy, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + - if params[:local] == '1' + .batch-table__form.simple_form + .fields-row + .fields-group.fields-row__column.fields-row__column-6 + .input.select.optional + .label_input + = f.select :category_id, options_from_collection_for_select(CustomEmojiCategory.all, 'id', 'name'), prompt: t('admin.custom_emojis.assign_category'), class: 'select optional', 'aria-label': t('admin.custom_emojis.assign_category') + + .fields-group.fields-row__column.fields-row__column-6 + .input.string.optional + .label_input + = f.text_field :category_name, class: 'string optional', placeholder: t('admin.custom_emojis.create_new_category'), 'aria-label': t('admin.custom_emojis.create_new_category') + + .batch-table__body + - if @custom_emojis.empty? + = nothing_here 'nothing-here--under-tabs' + - else + = render partial: 'custom_emoji', collection: @custom_emojis, locals: { f: f } = paginate @custom_emojis + +%hr.spacer/ + = link_to t('admin.custom_emojis.upload'), new_admin_custom_emoji_path, class: 'button' diff --git a/config/locales/en.yml b/config/locales/en.yml index 42d8e0eb8..52cb4a269 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -225,10 +225,12 @@ en: deleted_status: "(deleted status)" title: Audit log custom_emojis: + assign_category: Assign category by_domain: Domain copied_msg: Successfully created local copy of the emoji copy: Copy copy_failed_msg: Could not make a local copy of that emoji + create_new_category: Create new category created_msg: Emoji successfully created! delete: Delete destroyed_msg: Emojo successfully destroyed! @@ -245,6 +247,7 @@ en: shortcode: Shortcode shortcode_hint: At least 2 characters, only alphanumeric characters and underscores title: Custom emojis + uncategorized: Uncategorized unlisted: Unlisted update_failed_msg: Could not update that emoji updated_msg: Emoji successfully updated! diff --git a/config/routes.rb b/config/routes.rb index 534e68814..d22a9e56a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -242,11 +242,9 @@ Rails.application.routes.draw do resource :two_factor_authentication, only: [:destroy] end - resources :custom_emojis, only: [:index, :new, :create, :update, :destroy] do - member do - post :copy - post :enable - post :disable + resources :custom_emojis, only: [:index, :new, :create] do + collection do + post :batch end end diff --git a/spec/controllers/admin/custom_emojis_controller_spec.rb b/spec/controllers/admin/custom_emojis_controller_spec.rb index b7e2894e9..a8d96948c 100644 --- a/spec/controllers/admin/custom_emojis_controller_spec.rb +++ b/spec/controllers/admin/custom_emojis_controller_spec.rb @@ -52,64 +52,4 @@ describe Admin::CustomEmojisController do end end end - - describe 'PUT #update' do - let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test') } - let(:image) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'files', 'emojo.png'), 'image/png') } - - before do - put :update, params: { id: custom_emoji.id, custom_emoji: params } - end - - context 'when parameter is valid' do - let(:params) { { shortcode: 'updated', image: image } } - - it 'succeeds in updating custom emoji' do - expect(flash[:notice]).to eq I18n.t('admin.custom_emojis.updated_msg') - expect(custom_emoji.reload).to have_attributes(shortcode: 'updated') - end - end - - context 'when parameter is invalid' do - let(:params) { { shortcode: 'u', image: image } } - - it 'fails to update custom emoji' do - expect(flash[:alert]).to eq I18n.t('admin.custom_emojis.update_failed_msg') - expect(custom_emoji.reload).to have_attributes(shortcode: 'test') - end - end - end - - describe 'POST #copy' do - subject { post :copy, params: { id: custom_emoji.id } } - - let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test') } - - it 'copies custom emoji' do - expect { subject }.to change { CustomEmoji.where(shortcode: 'test').count }.by(1) - expect(flash[:notice]).to eq I18n.t('admin.custom_emojis.copied_msg') - end - end - - describe 'POST #enable' do - let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test', disabled: true) } - - before { post :enable, params: { id: custom_emoji.id } } - - it 'enables custom emoji' do - expect(response).to redirect_to admin_custom_emojis_path - expect(custom_emoji.reload).to have_attributes(disabled: false) - end - end - - describe 'POST #disable' do - let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test', disabled: false) } - - before { post :disable, params: { id: custom_emoji.id } } - - it 'enables custom emoji' do - expect(response).to redirect_to admin_custom_emojis_path - expect(custom_emoji.reload).to have_attributes(disabled: true) - end - end end -- cgit From 4faaa5b25e54f863fdf3a68edc5ca664e5a84e4c Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 10 Sep 2019 20:56:42 +0200 Subject: Add updated relationship to follow request API responses (#11800) Fixes #11747 --- app/controllers/api/v1/follow_requests_controller.rb | 8 ++++++-- spec/controllers/api/v1/follow_requests_controller_spec.rb | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) (limited to 'app/controllers') diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index e6888154e..0ee6e531f 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -14,12 +14,12 @@ class Api::V1::FollowRequestsController < Api::BaseController def authorize AuthorizeFollowService.new.call(account, current_account) NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account)) - render_empty + render json: account, serializer: REST::RelationshipSerializer, relationships: relationships end def reject RejectFollowService.new.call(account, current_account) - render_empty + render json: account, serializer: REST::RelationshipSerializer, relationships: relationships end private @@ -28,6 +28,10 @@ class Api::V1::FollowRequestsController < Api::BaseController Account.find(params[:id]) end + def relationships(**options) + AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, options) + end + def load_accounts default_accounts.merge(paginated_follow_requests).to_a end diff --git a/spec/controllers/api/v1/follow_requests_controller_spec.rb b/spec/controllers/api/v1/follow_requests_controller_spec.rb index 87292d9ce..ae92a9627 100644 --- a/spec/controllers/api/v1/follow_requests_controller_spec.rb +++ b/spec/controllers/api/v1/follow_requests_controller_spec.rb @@ -38,6 +38,12 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do it 'allows follower to follow' do expect(follower.following?(user.account)).to be true end + + it 'returns JSON with followed_by=true' do + json = body_as_json + + expect(json[:followed_by]).to be true + end end describe 'POST #reject' do @@ -54,5 +60,11 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do it 'removes follow request' do expect(FollowRequest.where(target_account: user.account, account: follower).count).to eq 0 end + + it 'returns JSON with followed_by=false' do + json = body_as_json + + expect(json[:followed_by]).to be false + end end end -- cgit From c5d37f18cb3f4d6212fb8f3e1c4e1e027f677ec5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 11 Sep 2019 16:32:44 +0200 Subject: Change deletes to preserve soft-deleted statuses in unresolved reports (#11805) Change all account actions except "none" to resolve all unresolved reports Refactor `SuspendAccountService` to be more readable --- app/controllers/admin/accounts_controller.rb | 2 +- app/controllers/admin/report_notes_controller.rb | 9 ++-- .../api/v1/admin/accounts_controller.rb | 2 +- app/lib/activitypub/activity/delete.rb | 3 +- app/models/account.rb | 1 + app/models/admin/account_action.rb | 24 +++++++-- app/models/form/account_batch.rb | 2 +- app/models/form/status_batch.rb | 2 +- app/models/report.rb | 1 + app/models/status.rb | 4 ++ app/models/user.rb | 4 ++ app/services/block_domain_service.rb | 2 +- app/services/remove_status_service.rb | 7 +-- app/services/suspend_account_service.rb | 62 ++++++++++++++++------ app/services/unallow_domain_service.rb | 2 +- app/workers/admin/suspension_worker.rb | 2 +- lib/mastodon/accounts_cli.rb | 4 +- lib/mastodon/domains_cli.rb | 2 +- .../admin/reported_statuses_controller_spec.rb | 2 +- spec/controllers/admin/statuses_controller_spec.rb | 2 +- spec/models/form/status_batch_spec.rb | 4 +- 21 files changed, 98 insertions(+), 45 deletions(-) (limited to 'app/controllers') diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 2fa1dfe5f..68b6352f8 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -41,7 +41,7 @@ module Admin def reject authorize @account.user, :reject? - SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) + SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false) redirect_to admin_pending_accounts_path end diff --git a/app/controllers/admin/report_notes_controller.rb b/app/controllers/admin/report_notes_controller.rb index bcb3f2026..b816c5b5d 100644 --- a/app/controllers/admin/report_notes_controller.rb +++ b/app/controllers/admin/report_notes_controller.rb @@ -5,10 +5,10 @@ module Admin before_action :set_report_note, only: [:destroy] def create - authorize ReportNote, :create? + authorize :report_note, :create? @report_note = current_account.report_notes.new(resource_params) - @report = @report_note.report + @report = @report_note.report if @report_note.save if params[:create_and_resolve] @@ -26,9 +26,8 @@ module Admin redirect_to admin_report_path(@report), notice: I18n.t('admin.report_notes.created_msg') else - @report_notes = @report.notes.latest - @report_history = @report.history - @form = Form::StatusBatch.new + @report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at) + @form = Form::StatusBatch.new render template: 'admin/reports/show' end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index c306180ca..c35ea5ab2 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -58,7 +58,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController def reject authorize @account.user, :reject? - SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) + SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false) render json: @account, serializer: REST::Admin::AccountSerializer end diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 345060462..dc9ff580c 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -13,8 +13,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity def delete_person lock_or_return("delete_in_progress:#{@account.id}") do - SuspendAccountService.new.call(@account) - @account.destroy! + SuspendAccountService.new.call(@account, reserve_username: false) end end diff --git a/app/models/account.rb b/app/models/account.rb index 8c9388b95..55fe53fae 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -115,6 +115,7 @@ class Account < ApplicationRecord :approved?, :pending?, :disabled?, + :unconfirmed_or_pending?, :role, :admin?, :moderator?, diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb index c7da8b52c..b30a82369 100644 --- a/app/models/admin/account_action.rb +++ b/app/models/admin/account_action.rb @@ -83,19 +83,23 @@ class Admin::AccountAction # A log entry is only interesting if the warning contains # custom text from someone. Otherwise it's just noise. + log_action(:create, warning) if warning.text.present? end def process_reports! - return if report_id.blank? + # If we're doing "mark as resolved" on a single report, + # then we want to keep other reports open in case they + # contain new actionable information. + # + # Otherwise, we will mark all unresolved reports about + # the account as resolved. - authorize(report, :update?) + reports.each { |report| authorize(report, :update?) } - if type == 'none' + reports.each do |report| log_action(:resolve, report) report.resolve!(current_account) - else - Report.where(target_account: target_account).unresolved.update_all(action_taken: true, action_taken_by_account_id: current_account.id) end end @@ -141,6 +145,16 @@ class Admin::AccountAction @report.status_ids if @report && include_statuses end + def reports + @reports ||= begin + if type == 'none' && with_report? + [report] + else + Report.where(target_account: target_account).unresolved + end + end + end + def warning_preset @warning_preset ||= AccountWarningPreset.find(warning_preset_id) if warning_preset_id.present? end diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index f1b7a4566..0b285fde9 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -69,6 +69,6 @@ class Form::AccountBatch records = accounts.includes(:user) records.each { |account| authorize(account.user, :reject?) } - .each { |account| SuspendAccountService.new.call(account, including_user: true, destroy: true, skip_distribution: true) } + .each { |account| SuspendAccountService.new.call(account, reserve_email: false, reserve_username: false) } end end diff --git a/app/models/form/status_batch.rb b/app/models/form/status_batch.rb index e09cc2594..c4943a7ea 100644 --- a/app/models/form/status_batch.rb +++ b/app/models/form/status_batch.rb @@ -35,7 +35,7 @@ class Form::StatusBatch def delete_statuses Status.where(id: status_ids).reorder(nil).find_each do |status| status.discard - RemovalWorker.perform_async(status.id, redraft: false) + RemovalWorker.perform_async(status.id, immediate: true) Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true) log_action :destroy, status end diff --git a/app/models/report.rb b/app/models/report.rb index 1e707ff1c..fb2e040ee 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -59,6 +59,7 @@ class Report < ApplicationRecord end def resolve!(acting_account) + RemovalWorker.push_bulk(Status.with_discarded.discarded.where(id: status_ids).pluck(:id)) { |status_id| [status_id, { immediate: true }] } update!(action_taken: true, action_taken_by_account_id: acting_account.id) end diff --git a/app/models/status.rb b/app/models/status.rb index 9cfaddcec..471bb03b4 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -214,6 +214,10 @@ class Status < ApplicationRecord !sensitive? && with_media? end + def reported? + @reported ||= Report.where(target_account: account).unresolved.where('? = ANY(status_ids)', id).exists? + end + def emojis return @emojis if defined?(@emojis) diff --git a/app/models/user.rb b/app/models/user.rb index 95f1d8fc5..78b82a68f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -171,6 +171,10 @@ class User < ApplicationRecord confirmed? && approved? && !disabled? && !account.suspended? end + def unconfirmed_or_pending? + !(confirmed? && approved?) + end + def inactive_message !approved? ? :pending : super end diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index 0ec6be503..ae461abf2 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -53,7 +53,7 @@ class BlockDomainService < BaseService def suspend_accounts! blocked_domain_accounts.without_suspended.reorder(nil).find_each do |account| - SuspendAccountService.new.call(account, suspended_at: @domain_block.created_at) + SuspendAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at) end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 685c1d4bf..f9352ed3d 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -8,7 +8,8 @@ class RemoveStatusService < BaseService # @param [Status] status # @param [Hash] options # @option [Boolean] :redraft - # @options [Boolean] :original_removed + # @option [Boolean] :immediate + # @option [Boolean] :original_removed def call(status, **options) @payload = Oj.dump(event: :delete, payload: status.id.to_s) @status = status @@ -31,7 +32,7 @@ class RemoveStatusService < BaseService remove_from_spam_check remove_media - @status.destroy! + @status.destroy! if @options[:immediate] || !@status.reported? else raise Mastodon::RaceConditionError end @@ -150,7 +151,7 @@ class RemoveStatusService < BaseService end def remove_media - return if @options[:redraft] + return if @options[:redraft] || (!@options[:immediate] && @status.reported?) @status.media_attachments.destroy_all end diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 85da7e921..ecc893931 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -15,7 +15,6 @@ class SuspendAccountService < BaseService favourites follow_requests list_accounts - media_attachments mute_relationships muted_by_relationships notifications @@ -32,14 +31,26 @@ class SuspendAccountService < BaseService targeted_reports ).freeze - # Suspend an account and remove as much of its data as possible + # Suspend or remove an account and remove as much of its data + # as possible. If it's a local account and it has not been confirmed + # or never been approved, then side effects are skipped and both + # the user and account records are removed fully. Otherwise, + # it is controlled by options. # @param [Account] # @param [Hash] options - # @option [Boolean] :including_user Remove the user record as well - # @option [Boolean] :destroy Remove the account record instead of suspending + # @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts + # @option [Boolean] :reserve_username Keep account record + # @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads + # @option [Time] :suspended_at Only applicable when :reserve_username is true def call(account, **options) @account = account - @options = options + @options = { reserve_username: true, reserve_email: true }.merge(options) + + if @account.local? && @account.user_unconfirmed_or_pending? + @options[:reserve_email] = false + @options[:reserve_username] = false + @options[:skip_side_effects] = true + end reject_follows! purge_user! @@ -60,27 +71,39 @@ class SuspendAccountService < BaseService def purge_user! return if !@account.local? || @account.user.nil? - if @options[:including_user] - @options[:destroy] = true if !@account.user_confirmed? || @account.user_pending? - @account.user.destroy - else + if @options[:reserve_email] @account.user.disable! @account.user.invites.where(uses: 0).destroy_all + else + @account.user.destroy end end def purge_content! - distribute_delete_actor! if @account.local? && !@options[:skip_distribution] + distribute_delete_actor! if @account.local? && !@options[:skip_side_effects] @account.statuses.reorder(nil).find_in_batches do |statuses| - BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:destroy]) + statuses.reject! { |status| reported_status_ids.include?(status.id) } if @options[:reserve_username] + BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:skip_side_effects]) + end + + @account.media_attachments.reorder(nil).find_each do |media_attachment| + next if @options[:reserve_username] && reported_status_ids.include?(media_attachment.status_id) + + media_attachment.destroy + end + + @account.polls.reorder(nil).find_each do |poll| + next if @options[:reserve_username] && reported_status_ids.include?(poll.status_id) + + poll.destroy end associations_for_destruction.each do |association_name| destroy_all(@account.public_send(association_name)) end - @account.destroy if @options[:destroy] + @account.destroy unless @options[:reserve_username] end def purge_profile! @@ -88,11 +111,13 @@ class SuspendAccountService < BaseService # there is no point wasting time updating # its values first - return if @options[:destroy] + return unless @options[:reserve_username] @account.silenced_at = nil @account.suspended_at = @options[:suspended_at] || Time.now.utc @account.locked = false + @account.memorial = false + @account.discoverable = false @account.display_name = '' @account.note = '' @account.fields = [] @@ -100,6 +125,7 @@ class SuspendAccountService < BaseService @account.followers_count = 0 @account.following_count = 0 @account.moved_to_account = nil + @account.trust_level = :untrusted @account.avatar.destroy @account.header.destroy @account.save! @@ -135,11 +161,15 @@ class SuspendAccountService < BaseService Account.inboxes - delivery_inboxes end + def reported_status_ids + @reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq + end + def associations_for_destruction - if @options[:destroy] - ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY - else + if @options[:reserve_username] ASSOCIATIONS_ON_SUSPEND + else + ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY end end end diff --git a/app/services/unallow_domain_service.rb b/app/services/unallow_domain_service.rb index d4387c1a1..bd1ad328d 100644 --- a/app/services/unallow_domain_service.rb +++ b/app/services/unallow_domain_service.rb @@ -3,7 +3,7 @@ class UnallowDomainService < BaseService def call(domain_allow) Account.where(domain: domain_allow.domain).find_each do |account| - SuspendAccountService.new.call(account, destroy: true) + SuspendAccountService.new.call(account, reserve_username: false) end domain_allow.destroy diff --git a/app/workers/admin/suspension_worker.rb b/app/workers/admin/suspension_worker.rb index ae8b24d8c..83c815efd 100644 --- a/app/workers/admin/suspension_worker.rb +++ b/app/workers/admin/suspension_worker.rb @@ -6,6 +6,6 @@ class Admin::SuspensionWorker sidekiq_options queue: 'pull' def perform(account_id, remove_user = false) - SuspendAccountService.new.call(Account.find(account_id), including_user: remove_user) + SuspendAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: !remove_user) end end diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index b16bf2e38..a09a6ab04 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -185,7 +185,7 @@ module Mastodon end say("Deleting user with #{account.statuses_count} statuses, this might take a while...") - SuspendAccountService.new.call(account, including_user: true) + SuspendAccountService.new.call(account, reserve_email: false) say('OK', :green) end @@ -239,7 +239,7 @@ module Mastodon end if [404, 410].include?(code) - SuspendAccountService.new.call(account, destroy: true) unless options[:dry_run] + SuspendAccountService.new.call(account, reserve_username: false) unless options[:dry_run] 1 else # Touch account even during dry run to avoid getting the account into the window again diff --git a/lib/mastodon/domains_cli.rb b/lib/mastodon/domains_cli.rb index c612c2d72..8e52de1c3 100644 --- a/lib/mastodon/domains_cli.rb +++ b/lib/mastodon/domains_cli.rb @@ -42,7 +42,7 @@ module Mastodon end processed, = parallelize_with_progress(scope) do |account| - SuspendAccountService.new.call(account, destroy: true) unless options[:dry_run] + SuspendAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run] end DomainBlock.where(domain: domain).destroy_all unless options[:dry_run] diff --git a/spec/controllers/admin/reported_statuses_controller_spec.rb b/spec/controllers/admin/reported_statuses_controller_spec.rb index bd146b795..2a1598123 100644 --- a/spec/controllers/admin/reported_statuses_controller_spec.rb +++ b/spec/controllers/admin/reported_statuses_controller_spec.rb @@ -47,7 +47,7 @@ describe Admin::ReportedStatusesController do it 'removes a status' do allow(RemovalWorker).to receive(:perform_async) subject.call - expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, redraft: false) + expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, immediate: true) end end diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb index 6b06343ef..d9690d83f 100644 --- a/spec/controllers/admin/statuses_controller_spec.rb +++ b/spec/controllers/admin/statuses_controller_spec.rb @@ -65,7 +65,7 @@ describe Admin::StatusesController do it 'removes a status' do allow(RemovalWorker).to receive(:perform_async) subject.call - expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, redraft: false) + expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, immediate: true) end end diff --git a/spec/models/form/status_batch_spec.rb b/spec/models/form/status_batch_spec.rb index f9c58c90f..68d84a737 100644 --- a/spec/models/form/status_batch_spec.rb +++ b/spec/models/form/status_batch_spec.rb @@ -41,12 +41,12 @@ describe Form::StatusBatch do it 'call RemovalWorker' do form.save - expect(RemovalWorker).to have_received(:perform_async).with(status.id, redraft: false) + expect(RemovalWorker).to have_received(:perform_async).with(status.id, immediate: true) end it 'do not call RemovalWorker' do form.save - expect(RemovalWorker).not_to have_received(:perform_async).with(another_status.id, redraft: false) + expect(RemovalWorker).not_to have_received(:perform_async).with(another_status.id, immediate: true) end end end -- cgit From 16cdaa1110878f84a542f0f9a6b323ba800f4e81 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 12 Sep 2019 01:51:12 +0200 Subject: Fix uncaught errors in media proxy controller (#11811) --- app/controllers/media_proxy_controller.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app/controllers') diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 558cd6e30..47544f21c 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -8,6 +8,8 @@ class MediaProxyController < ApplicationController before_action :authenticate_user!, if: :whitelist_mode? rescue_from ActiveRecord::RecordInvalid, with: :not_found + rescue_from Mastodon::UnexpectedResponseError, with: :not_found + rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error def show RedisLock.acquire(lock_options) do |lock| -- cgit From 59da5ccb8e9e06e0c91ebd548e899786c632f1de Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 13 Sep 2019 16:01:09 +0200 Subject: Change unlisted custom emoji to not appear in autosuggestions (#11818) Fix #11669 --- app/controllers/api/v1/custom_emojis_controller.rb | 2 +- app/models/custom_emoji.rb | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'app/controllers') diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb index 252f667dd..4e6d5d7c6 100644 --- a/app/controllers/api/v1/custom_emojis_controller.rb +++ b/app/controllers/api/v1/custom_emojis_controller.rb @@ -7,6 +7,6 @@ class Api::V1::CustomEmojisController < Api::BaseController def index expires_in 3.minutes, public: true - render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.local.where(disabled: false).includes(:category) } + render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) } end end diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 0a4201a14..edb1bec75 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -40,10 +40,11 @@ class CustomEmoji < ApplicationRecord validates_attachment :image, content_type: { content_type: IMAGE_MIME_TYPES }, presence: true, size: { less_than: LIMIT } validates :shortcode, uniqueness: { scope: :domain }, format: { with: /\A#{SHORTCODE_RE_FRAGMENT}\z/ }, length: { minimum: 2 } - scope :local, -> { where(domain: nil) } - scope :remote, -> { where.not(domain: nil) } + scope :local, -> { where(domain: nil) } + scope :remote, -> { where.not(domain: nil) } scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) } scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) } + scope :listed, -> { local.where(disabled: false).where(visible_in_picker: true) } remotable_attachment :image, LIMIT -- cgit From 225edac118634b146c1bbf45f199d8a1257bcf56 Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 13 Sep 2019 16:03:46 +0200 Subject: Change /api/v1/timelines/public to require auth when public preview is off (#11802) Fixes #11289 --- app/controllers/api/v1/timelines/public_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'app/controllers') diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index aabe24324..ccc10f966 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Api::V1::Timelines::PublicController < Api::BaseController + before_action :require_user!, only: [:show], if: :require_auth? after_action :insert_pagination_headers, unless: -> { @statuses.empty? } respond_to :json @@ -12,6 +13,10 @@ class Api::V1::Timelines::PublicController < Api::BaseController private + def require_auth? + !Setting.timeline_preview + end + def load_statuses cached_public_statuses end -- cgit From 18331fefa2246facc818226043b1f9cc67cf6c1a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 13 Sep 2019 16:11:13 +0200 Subject: Remove deprecated `GET /api/v1/search` API (#11823) Use `GET /api/v2/search` instead --- app/controllers/api/v1/search_controller.rb | 32 ----------------------- app/controllers/api/v2/search_controller.rb | 28 ++++++++++++++++++-- app/serializers/rest/search_serializer.rb | 7 +---- app/serializers/rest/v2/search_serializer.rb | 7 ----- config/routes.rb | 2 -- spec/controllers/api/v1/search_controller_spec.rb | 22 ---------------- 6 files changed, 27 insertions(+), 71 deletions(-) delete mode 100644 app/controllers/api/v1/search_controller.rb delete mode 100644 app/serializers/rest/v2/search_serializer.rb delete mode 100644 spec/controllers/api/v1/search_controller_spec.rb (limited to 'app/controllers') diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb deleted file mode 100644 index 6131cbbb6..000000000 --- a/app/controllers/api/v1/search_controller.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::SearchController < Api::BaseController - include Authorization - - RESULTS_LIMIT = 20 - - before_action -> { doorkeeper_authorize! :read, :'read:search' } - before_action :require_user! - - respond_to :json - - def index - @search = Search.new(search_results) - render json: @search, serializer: REST::SearchSerializer - end - - private - - def search_results - SearchService.new.call( - params[:q], - current_account, - limit_param(RESULTS_LIMIT), - search_params.merge(resolve: truthy_param?(:resolve)) - ) - end - - def search_params - params.permit(:type, :offset, :min_id, :max_id, :account_id) - end -end diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index 9aa6edc69..c14cd22d7 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -1,8 +1,32 @@ # frozen_string_literal: true -class Api::V2::SearchController < Api::V1::SearchController +class Api::V2::SearchController < Api::BaseController + include Authorization + + RESULTS_LIMIT = 20 + + before_action -> { doorkeeper_authorize! :read, :'read:search' } + before_action :require_user! + + respond_to :json + def index @search = Search.new(search_results) - render json: @search, serializer: REST::V2::SearchSerializer + render json: @search, serializer: REST::SearchSerializer + end + + private + + def search_results + SearchService.new.call( + params[:q], + current_account, + limit_param(RESULTS_LIMIT), + search_params.merge(resolve: truthy_param?(:resolve)) + ) + end + + def search_params + params.permit(:type, :offset, :min_id, :max_id, :account_id) end end diff --git a/app/serializers/rest/search_serializer.rb b/app/serializers/rest/search_serializer.rb index 157f543ae..ee9b421eb 100644 --- a/app/serializers/rest/search_serializer.rb +++ b/app/serializers/rest/search_serializer.rb @@ -1,12 +1,7 @@ # frozen_string_literal: true class REST::SearchSerializer < ActiveModel::Serializer - attributes :hashtags - has_many :accounts, serializer: REST::AccountSerializer has_many :statuses, serializer: REST::StatusSerializer - - def hashtags - object.hashtags.map(&:name) - end + has_many :hashtags, serializer: REST::TagSerializer end diff --git a/app/serializers/rest/v2/search_serializer.rb b/app/serializers/rest/v2/search_serializer.rb deleted file mode 100644 index cdb6b3a53..000000000 --- a/app/serializers/rest/v2/search_serializer.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class REST::V2::SearchSerializer < ActiveModel::Serializer - has_many :accounts, serializer: REST::AccountSerializer - has_many :statuses, serializer: REST::StatusSerializer - has_many :hashtags, serializer: REST::TagSerializer -end diff --git a/config/routes.rb b/config/routes.rb index d22a9e56a..a4dee2842 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -311,8 +311,6 @@ Rails.application.routes.draw do end end - get '/search', to: 'search#index', as: :search - resources :media, only: [:create, :update] resources :blocks, only: [:index] resources :mutes, only: [:index] diff --git a/spec/controllers/api/v1/search_controller_spec.rb b/spec/controllers/api/v1/search_controller_spec.rb deleted file mode 100644 index c9e544cc7..000000000 --- a/spec/controllers/api/v1/search_controller_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Api::V1::SearchController, type: :controller do - render_views - - let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:search') } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'GET #index' do - it 'returns http success' do - get :index, params: { q: 'test' } - - expect(response).to have_http_status(200) - end - end -end -- cgit