diff --git a/.codeclimate.yml b/.codeclimate.yml index 29701a777..47e3e6ab9 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,21 +1,36 @@ -engines: +version: "2" +checks: + argument-count: + enabled: false + complex-logic: + enabled: false + file-lines: + enabled: false + method-complexity: + enabled: false + method-count: + enabled: false + method-lines: + enabled: false + nested-control-flow: + enabled: false + return-statements: + enabled: false + similar-code: + enabled: false + identical-code: + enabled: false +plugins: brakeman: enabled: true bundler-audit: enabled: true - duplication: - enabled: false eslint: enabled: true rubocop: enabled: true scss-lint: enabled: true -ratings: - paths: - - "**.rb" - - "**.js" - - "**.scss" -exclude_paths: +exclude_patterns: - spec/ - vendor/asset diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 7cec57180..4b20fe971 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eugen@zeonfederated.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beatrix.bitrot@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. diff --git a/Gemfile.lock b/Gemfile.lock index f9c69d538..37fc77fdf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -24,11 +24,11 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - active_model_serializers (0.10.6) + active_model_serializers (0.10.7) actionpack (>= 4.1, < 6) activemodel (>= 4.1, < 6) case_transform (>= 0.2) - jsonapi-renderer (>= 0.1.1.beta1, < 0.2) + jsonapi-renderer (>= 0.1.1.beta1, < 0.3) active_record_query_trace (1.5.4) activejob (5.1.4) activesupport (= 5.1.4) @@ -83,15 +83,15 @@ GEM capistrano-bundler (1.3.0) capistrano (~> 3.1) sshkit (~> 1.2) - capistrano-rails (1.3.0) + capistrano-rails (1.3.1) capistrano (~> 3.1) capistrano-bundler (~> 1.1) - capistrano-rbenv (2.1.2) + capistrano-rbenv (2.1.3) capistrano (~> 3.1) sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (2.15.4) + capybara (2.16.1) addressable mini_mime (>= 0.1.3) nokogiri (>= 1.3.3) @@ -113,7 +113,7 @@ GEM connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) - crass (1.0.2) + crass (1.0.3) debug_inspector (0.0.3) devise (4.3.0) bcrypt (~> 3.0) @@ -121,11 +121,11 @@ GEM railties (>= 4.1.0, < 5.2) responders warden (~> 1.2.3) - devise-two-factor (3.0.0) - activesupport + devise-two-factor (3.0.2) + activesupport (< 5.2) attr_encrypted (>= 1.3, < 4, != 2) devise (~> 4.0) - railties + railties (< 5.2) rotp (~> 2.0) diff-lcs (1.3) docile (1.1.5) @@ -184,7 +184,7 @@ GEM http (~> 2.2) nokogiri (~> 1.8) oj (~> 3.0) - hamlit (2.8.4) + hamlit (2.8.5) temple (>= 0.8.0) thor tilt @@ -196,7 +196,7 @@ GEM hamster (3.0.0) concurrent-ruby (~> 1.0) hashdiff (0.3.7) - highline (1.7.8) + highline (1.7.10) hiredis (0.6.1) hkdf (0.3.0) htmlentities (4.3.4) @@ -213,9 +213,9 @@ GEM httplog (0.99.7) colorize rack - i18n (0.9.0) + i18n (0.9.1) concurrent-ruby (~> 1.0) - i18n-tasks (0.9.18) + i18n-tasks (0.9.19) activesupport (>= 4.0.2) ast (>= 2.1.0) easy_translate (>= 0.5.0) @@ -236,8 +236,8 @@ GEM json-ld (~> 2.1, >= 2.1.5) multi_json (~> 1.11) rdf (~> 2.2) - jsonapi-renderer (0.1.3) - jwt (1.5.6) + jsonapi-renderer (0.2.0) + jwt (2.1.0) kaminari (1.1.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.1.1) @@ -267,8 +267,8 @@ GEM loofah (2.1.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.6.6) - mime-types (>= 1.16, < 4) + mail (2.7.0) + mini_mime (>= 0.1.1) mario-redis-lock (1.2.0) redis (~> 3, >= 3.0.5) method_source (0.9.0) @@ -279,7 +279,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mimemagic (0.3.2) - mini_mime (0.1.4) + mini_mime (1.0.0) mini_portile2 (2.3.0) minitest (5.10.3) msgpack (1.1.0) @@ -305,7 +305,7 @@ GEM http (~> 2.0) nokogiri (~> 1.6) openssl (~> 2.0) - ox (2.8.1) + ox (2.8.2) paperclip (5.1.0) activemodel (>= 4.2.0) activesupport (>= 4.2.0) @@ -316,22 +316,22 @@ GEM av (~> 0.9.0) paperclip (>= 2.5.2) parallel (1.12.0) - parallel_tests (2.17.0) + parallel_tests (2.19.0) parallel - parser (2.4.0.0) - ast (~> 2.2) + parser (2.4.0.2) + ast (~> 2.3) pg (0.21.0) pghero (1.7.0) activerecord pkg-config (1.2.8) powerpack (0.1.1) - pry (0.11.2) + pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) pry-rails (0.3.6) pry (>= 0.10.4) - public_suffix (3.0.0) - puma (3.10.0) + public_suffix (3.0.1) + puma (3.11.0) pundit (1.1.0) activesupport (>= 3.0.0) rabl (0.13.1) @@ -344,7 +344,7 @@ GEM rack rack-proxy (0.6.2) rack - rack-test (0.7.0) + rack-test (0.8.2) rack (>= 1.0, < 3) rack-timeout (0.4.2) rails (5.1.4) @@ -381,8 +381,11 @@ GEM thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake - rake (12.2.1) - rdf (2.2.11) + rake (12.3.0) + rb-fsevent (0.10.2) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) + rdf (2.2.12) hamster (~> 3.0) link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.3.2) @@ -395,8 +398,8 @@ GEM redis-activesupport (5.0.4) activesupport (>= 3, < 6) redis-store (>= 1.3, < 2) - redis-namespace (1.5.3) - redis (~> 3.0, >= 3.0.4) + redis-namespace (1.6.0) + redis (>= 3.0.4) redis-rack (2.0.3) rack (>= 1.5, < 3) redis-store (>= 1.2, < 2) @@ -421,7 +424,7 @@ GEM rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.7.0) - rspec-rails (3.7.1) + rspec-rails (3.7.2) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) @@ -449,10 +452,14 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.4.4) nokogumbo (~> 1.4.1) - sass (3.4.25) - scss_lint (0.55.0) + sass (3.5.3) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + scss_lint (0.56.0) rake (>= 0.9, < 13) - sass (~> 3.4.20) + sass (~> 3.5.3) sidekiq (5.0.5) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) @@ -486,7 +493,7 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sshkit (1.14.0) + sshkit (1.15.1) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) statsd-ruby (1.2.1) @@ -514,7 +521,7 @@ GEM uniform_notifier (1.10.0) warden (1.2.7) rack (>= 1.0) - webmock (3.1.0) + webmock (3.1.1) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff @@ -522,12 +529,12 @@ GEM activesupport (>= 4.2) rack-proxy (>= 0.6.1) railties (>= 4.2) - webpush (0.3.2) + webpush (0.3.3) hkdf (~> 0.2) - jwt + jwt (~> 2.0) websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.2) + websocket-extensions (0.1.3) xpath (2.1.0) nokogiri (~> 1.3) @@ -638,4 +645,4 @@ RUBY VERSION ruby 2.4.2p198 BUNDLED WITH - 1.15.4 + 1.16.0 diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb index 7f69a3363..7d5b9bf52 100644 --- a/app/controllers/admin/account_moderation_notes_controller.rb +++ b/app/controllers/admin/account_moderation_notes_controller.rb @@ -21,7 +21,7 @@ module Admin def destroy authorize @account_moderation_note, :destroy? - @account_moderation_note.destroy + @account_moderation_note.destroy! redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg') end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 0829bc769..e9a512e70 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -32,18 +32,21 @@ module Admin def memorialize authorize @account, :memorialize? @account.memorialize! + log_action :memorialize, @account redirect_to admin_account_path(@account.id) end def enable authorize @account.user, :enable? @account.user.enable! + log_action :enable, @account.user redirect_to admin_account_path(@account.id) end def disable authorize @account.user, :disable? @account.user.disable! + log_action :disable, @account.user redirect_to admin_account_path(@account.id) end diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb new file mode 100644 index 000000000..e273dfeae --- /dev/null +++ b/app/controllers/admin/action_logs_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Admin + class ActionLogsController < BaseController + def index + @action_logs = Admin::ActionLog.page(params[:page]) + end + end +end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 726134509..fc299f74c 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -3,6 +3,7 @@ module Admin class BaseController < ApplicationController include Authorization + include AccountableConcern layout 'admin' diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index c10b0ebee..34dfb458e 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -7,6 +7,7 @@ module Admin def create authorize @user, :confirm? @user.confirm! + log_action :confirm, @user redirect_to admin_accounts_path end diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index 509f7a48f..3fa2a0b72 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -20,6 +20,7 @@ module Admin @custom_emoji = CustomEmoji.new(resource_params) if @custom_emoji.save + log_action :create, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg') else render :new @@ -30,6 +31,7 @@ module Admin authorize @custom_emoji, :update? if @custom_emoji.update(resource_params) + log_action :update, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg') else redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg') @@ -38,7 +40,8 @@ module Admin def destroy authorize @custom_emoji, :destroy? - @custom_emoji.destroy + @custom_emoji.destroy! + log_action :destroy, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg') end @@ -49,6 +52,7 @@ module Admin 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') @@ -60,12 +64,14 @@ module Admin def enable authorize @custom_emoji, :enable? @custom_emoji.update!(disabled: false) + log_action :enable, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg') end def disable authorize @custom_emoji, :disable? @custom_emoji.update!(disabled: true) + log_action :disable, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg') end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index e383dc831..64de2cbf0 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -21,6 +21,7 @@ module Admin if @domain_block.save DomainBlockWorker.perform_async(@domain_block.id) + log_action :create, @domain_block redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg') else render :new @@ -34,6 +35,7 @@ module Admin def destroy authorize @domain_block, :destroy? UnblockDomainService.new.call(@domain_block, retroactive_unblock?) + log_action :destroy, @domain_block redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg') end diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index 01058bf46..9fe85064e 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -20,6 +20,7 @@ module Admin @email_domain_block = EmailDomainBlock.new(resource_params) if @email_domain_block.save + log_action :create, @email_domain_block redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg') else render :new @@ -28,7 +29,8 @@ module Admin def destroy authorize @email_domain_block, :destroy? - @email_domain_block.destroy + @email_domain_block.destroy! + log_action :destroy, @email_domain_block redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg') end diff --git a/app/controllers/admin/invites_controller.rb b/app/controllers/admin/invites_controller.rb new file mode 100644 index 000000000..faccaa7c8 --- /dev/null +++ b/app/controllers/admin/invites_controller.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Admin + class InvitesController < BaseController + def index + authorize :invite, :index? + + @invites = filtered_invites.includes(user: :account).page(params[:page]) + @invite = Invite.new + end + + def create + authorize :invite, :create? + + @invite = Invite.new(resource_params) + @invite.user = current_user + + if @invite.save + redirect_to admin_invites_path + else + @invites = Invite.page(params[:page]) + render :index + end + end + + def destroy + @invite = Invite.find(params[:id]) + authorize @invite, :destroy? + @invite.expire! + redirect_to admin_invites_path + end + + private + + def resource_params + params.require(:invite).permit(:max_uses, :expires_in) + end + + def filtered_invites + InviteFilter.new(filter_params).results + end + + def filter_params + params.permit(:available, :expired) + end + end +end diff --git a/app/controllers/admin/reported_statuses_controller.rb b/app/controllers/admin/reported_statuses_controller.rb index 4f66ce708..535bd11d4 100644 --- a/app/controllers/admin/reported_statuses_controller.rb +++ b/app/controllers/admin/reported_statuses_controller.rb @@ -8,7 +8,7 @@ module Admin def create authorize :status, :update? - @form = Form::StatusBatch.new(form_status_batch_params) + @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account)) flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save redirect_to admin_report_path(@report) @@ -16,13 +16,15 @@ module Admin def update authorize @status, :update? - @status.update(status_params) + @status.update!(status_params) + log_action :update, @status redirect_to admin_report_path(@report) end def destroy authorize @status, :destroy? RemovalWorker.perform_async(@status.id) + log_action :destroy, @status render json: @status end diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 745757ee8..75db6b78a 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -25,12 +25,17 @@ module Admin def process_report case params[:outcome].to_s when 'resolve' - @report.update(action_taken_by_current_attributes) + @report.update!(action_taken_by_current_attributes) + log_action :resolve, @report when 'suspend' Admin::SuspensionWorker.perform_async(@report.target_account.id) + log_action :resolve, @report + log_action :suspend, @report.target_account resolve_all_target_account_reports when 'silence' - @report.target_account.update(silenced: true) + @report.target_account.update!(silenced: true) + log_action :resolve, @report + log_action :silence, @report.target_account resolve_all_target_account_reports else raise ActiveRecord::RecordNotFound diff --git a/app/controllers/admin/resets_controller.rb b/app/controllers/admin/resets_controller.rb index 00b590bf6..3e27d01ac 100644 --- a/app/controllers/admin/resets_controller.rb +++ b/app/controllers/admin/resets_controller.rb @@ -7,6 +7,7 @@ module Admin def create authorize @user, :reset_password? @user.send_reset_password_instructions + log_action :reset_password, @user redirect_to admin_accounts_path end diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index 8f8685827..af7ec0740 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -7,12 +7,14 @@ module Admin def promote authorize @user, :promote? @user.promote! + log_action :promote, @user redirect_to admin_account_path(@user.account_id) end def demote authorize @user, :demote? @user.demote! + log_action :demote, @user redirect_to admin_account_path(@user.account_id) end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index e81290228..eed5fb6b5 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -13,14 +13,17 @@ module Admin closed_registrations_message open_deletion timeline_preview + show_staff_badge bootstrap_timeline_accounts thumbnail + min_invite_role ).freeze BOOLEAN_SETTINGS = %w( open_registrations open_deletion timeline_preview + show_staff_badge ).freeze UPLOAD_SETTINGS = %w( diff --git a/app/controllers/admin/silences_controller.rb b/app/controllers/admin/silences_controller.rb index 01fb292de..4c06a9c0c 100644 --- a/app/controllers/admin/silences_controller.rb +++ b/app/controllers/admin/silences_controller.rb @@ -6,13 +6,15 @@ module Admin def create authorize @account, :silence? - @account.update(silenced: true) + @account.update!(silenced: true) + log_action :silence, @account redirect_to admin_accounts_path end def destroy authorize @account, :unsilence? - @account.update(silenced: false) + @account.update!(silenced: false) + log_action :unsilence, @account redirect_to admin_accounts_path end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index b54a9b824..5d4325f57 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -26,7 +26,7 @@ module Admin def create authorize :status, :update? - @form = Form::StatusBatch.new(form_status_batch_params) + @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account)) flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save redirect_to admin_account_statuses_path(@account.id, current_params) @@ -34,13 +34,15 @@ module Admin def update authorize @status, :update? - @status.update(status_params) + @status.update!(status_params) + log_action :update, @status redirect_to admin_account_statuses_path(@account.id, current_params) end def destroy authorize @status, :destroy? RemovalWorker.perform_async(@status.id) + log_action :destroy, @status render json: @status end diff --git a/app/controllers/admin/suspensions_controller.rb b/app/controllers/admin/suspensions_controller.rb index 778feea5e..5f222e125 100644 --- a/app/controllers/admin/suspensions_controller.rb +++ b/app/controllers/admin/suspensions_controller.rb @@ -7,12 +7,14 @@ module Admin def create authorize @account, :suspend? Admin::SuspensionWorker.perform_async(@account.id) + log_action :suspend, @account redirect_to admin_accounts_path end def destroy authorize @account, :unsuspend? @account.unsuspend! + log_action :unsuspend, @account redirect_to admin_accounts_path end diff --git a/app/controllers/admin/two_factor_authentications_controller.rb b/app/controllers/admin/two_factor_authentications_controller.rb index 5a45d25cd..022107203 100644 --- a/app/controllers/admin/two_factor_authentications_controller.rb +++ b/app/controllers/admin/two_factor_authentications_controller.rb @@ -7,6 +7,7 @@ module Admin def destroy authorize @user, :disable_2fa? @user.disable_two_factor! + log_action :disable_2fa, @user redirect_to admin_accounts_path end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 85eb2d60e..b1a2ed573 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -13,11 +13,9 @@ class Api::V1::AccountsController < Api::BaseController end def follow - reblogs_arg = { reblogs: params[:reblogs] } - - FollowService.new.call(current_user.account, @account.acct, reblogs_arg) + FollowService.new.call(current_user.account, @account.acct, reblogs: params[:reblogs]) - options = @account.locked? ? {} : { following_map: { @account.id => reblogs_arg }, requested_map: { @account.id => false } } + options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: params[:reblogs] } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 42e852c04..f4247fd95 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -17,13 +17,16 @@ class Auth::RegistrationsController < Devise::RegistrationsController def build_resource(hash = nil) super(hash) - resource.locale = I18n.locale + + resource.locale = I18n.locale + resource.invite_code = params[:invite_code] if resource.invite_code.blank? + resource.build_account if resource.account.nil? end def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up) do |u| - u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation) + u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation, :invite_code) end end @@ -36,7 +39,19 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def check_enabled_registrations - redirect_to root_path if single_user_mode? || !Setting.open_registrations + redirect_to root_path if single_user_mode? || !allowed_registrations? + end + + def allowed_registrations? + Setting.open_registrations || (invite_code.present? && Invite.find_by(code: invite_code)&.valid_for_use?) + end + + def invite_code + if params[:user] + params[:user][:invite_code] + else + params[:invite_code] + end end private diff --git a/app/controllers/concerns/accountable_concern.rb b/app/controllers/concerns/accountable_concern.rb new file mode 100644 index 000000000..3cdcffc51 --- /dev/null +++ b/app/controllers/concerns/accountable_concern.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module AccountableConcern + extend ActiveSupport::Concern + + def log_action(action, target) + Admin::ActionLog.create(account: current_account, action: action, target: target) + end +end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb new file mode 100644 index 000000000..38d6c8d73 --- /dev/null +++ b/app/controllers/invites_controller.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class InvitesController < ApplicationController + include Authorization + + layout 'admin' + + before_action :authenticate_user! + + def index + authorize :invite, :create? + + @invites = Invite.where(user: current_user) + @invite = Invite.new(expires_in: 1.day.to_i) + end + + def create + authorize :invite, :create? + + @invite = Invite.new(resource_params) + @invite.user = current_user + + if @invite.save + redirect_to invites_path + else + @invites = Invite.where(user: current_user) + render :index + end + end + + def destroy + @invite = Invite.where(user: current_user).find(params[:id]) + authorize @invite, :destroy? + @invite.expire! + redirect_to invites_path + end + + private + + def resource_params + params.require(:invite).permit(:max_uses, :expires_in) + end +end diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb new file mode 100644 index 000000000..b18403a7f --- /dev/null +++ b/app/controllers/settings/migrations_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class Settings::MigrationsController < ApplicationController + layout 'admin' + + before_action :authenticate_user! + + def show + @migration = Form::Migration.new(account: current_account.moved_to_account) + end + + def update + @migration = Form::Migration.new(resource_params) + + if @migration.valid? && migration_account_changed? + current_account.update!(moved_to_account: @migration.account) + ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) + redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg') + else + render :show + end + end + + private + + def resource_params + params.require(:migration).permit(:acct) + end + + def migration_account_changed? + current_account.moved_to_account_id != @migration.account&.id + end +end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb new file mode 100644 index 000000000..e85243e57 --- /dev/null +++ b/app/helpers/admin/action_logs_helper.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module Admin::ActionLogsHelper + def log_target(log) + if log.target + linkable_log_target(log.target) + else + log_target_from_history(log.target_type, log.recorded_changes) + end + end + + def linkable_log_target(record) + case record.class.name + when 'Account' + link_to record.acct, admin_account_path(record.id) + when 'User' + link_to record.account.acct, admin_account_path(record.account_id) + when 'CustomEmoji' + record.shortcode + when 'Report' + link_to "##{record.id}", admin_report_path(record) + when 'DomainBlock', 'EmailDomainBlock' + link_to record.domain, "https://#{record.domain}" + when 'Status' + link_to record.account.acct, TagManager.instance.url_for(record) + end + end + + def log_target_from_history(type, attributes) + case type + when 'CustomEmoji' + attributes['shortcode'] + when 'DomainBlock', 'EmailDomainBlock' + link_to attributes['domain'], "https://#{attributes['domain']}" + when 'Status' + tmp_status = Status.new(attributes) + link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status) + end + end + + def relevant_log_changes(log) + if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action) + log.recorded_changes.slice('domain') + elsif log.target_type == 'CustomEmoji' && log.action == :update + log.recorded_changes.slice('domain', 'visible_in_picker') + elsif log.target_type == 'User' && [:promote, :demote].include?(log.action) + log.recorded_changes.slice('moderator', 'admin') + elsif log.target_type == 'DomainBlock' + log.recorded_changes.slice('severity', 'reject_media') + elsif log.target_type == 'Status' && log.action == :update + log.recorded_changes.slice('sensitive') + end + end + + def log_extra_attributes(hash) + safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ') + end + + def log_change(val) + return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array) + safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→') + end + + def icon_for_log(log) + case log.target_type + when 'Account', 'User' + 'user' + when 'CustomEmoji' + 'file' + when 'Report' + 'flag' + when 'DomainBlock' + 'lock' + when 'EmailDomainBlock' + 'envelope' + when 'Status' + 'pencil' + end + end + + def class_for_log_icon(log) + case log.action + when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve + 'positive' + when :create + opposite_verbs?(log) ? 'negative' : 'positive' + when :update, :reset_password, :disable_2fa, :memorialize + 'neutral' + when :demote, :silence, :disable, :suspend + 'negative' + when :destroy + opposite_verbs?(log) ? 'positive' : 'negative' + else + '' + end + end + + private + + def opposite_verbs?(log) + %w(DomainBlock EmailDomainBlock).include?(log.target_type) + end +end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index e0fae9d9a..73250cbf5 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -3,8 +3,9 @@ module Admin::FilterHelper ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip).freeze REPORT_FILTERS = %i(resolved account_id target_account_id).freeze + INVITE_FILTER = %i(available expired).freeze - FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER def filter_link_to(text, link_to_params, link_class_params = link_to_params) new_url = filtered_url_for(link_to_params) diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index a3441e6f9..6c7c38070 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -9,6 +9,24 @@ module JsonLdHelper value.is_a?(Array) ? value.first : value end + # The url attribute can be a string, an array of strings, or an array of objects. + # The objects could include a mimeType. Not-included mimeType means it's text/html. + def url_to_href(value, preferred_type = nil) + single_value = if value.is_a?(Array) && !value.first.is_a?(String) + value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) } + elsif value.is_a?(Array) + value.first + else + value + end + + if single_value.nil? || single_value.is_a?(String) + single_value + else + single_value['href'] + end + end + def as_array(value) value.is_a?(Array) ? value : [value] end diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 6662285d0..b0d9e3757 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -9,6 +9,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { MediaGallery, Video } from 'flavours/glitch/util/async-components'; import { HotKeys } from 'react-hotkeys'; import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; +import classNames from 'classnames'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -21,6 +22,7 @@ export default class Status extends ImmutablePureComponent { }; static propTypes = { + containerId: PropTypes.string, id: PropTypes.string, status: ImmutablePropTypes.map, account: ImmutablePropTypes.map, @@ -59,6 +61,7 @@ export default class Status extends ImmutablePureComponent { 'muted', 'collapse', 'notification', + 'hidden', ] updateOnStates = [ @@ -187,7 +190,9 @@ export default class Status extends ImmutablePureComponent { } handleExpandedToggle = () => { - this.setExpansion(this.state.isExpanded || !this.props.status.get('spoiler') ? null : true); + if (this.props.status.get('spoiler_text')) { + this.setExpansion(this.state.isExpanded ? null : true); + } }; handleOpenVideo = startTime => { @@ -221,11 +226,11 @@ export default class Status extends ImmutablePureComponent { } handleHotkeyMoveUp = () => { - this.props.onMoveUp(this.props.status.get('id')); + this.props.onMoveUp(this.props.containerId || this.props.id); } handleHotkeyMoveDown = () => { - this.props.onMoveDown(this.props.status.get('id')); + this.props.onMoveDown(this.props.containerId || this.props.id); } handleRef = c => { @@ -370,31 +375,24 @@ export default class Status extends ImmutablePureComponent { openProfile: this.handleHotkeyOpenProfile, moveUp: this.handleHotkeyMoveUp, moveDown: this.handleHotkeyMoveDown, + toggleSpoiler: this.handleExpandedToggle, }; + const computedClass = classNames('status', `status-${status.get('visibility')}`, { + collapsed: isExpanded === false, + 'has-background': isExpanded === false && background, + 'marked-for-delete': this.state.markedForDelete, + muted, + }, 'focusable'); + return (
{prepend && account ? ( { } return { + containerId : props.containerId || props.id, // Should match reblogStatus's id for reblogs status : status, account : account || props.account, settings : state.get('local_settings'), diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow.js b/app/javascript/flavours/glitch/features/notifications/components/follow.js index 4b682733e..54506f67c 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/follow.js +++ b/app/javascript/flavours/glitch/features/notifications/components/follow.js @@ -14,6 +14,7 @@ import NotificationOverlayContainer from '../containers/overlay_container'; export default class NotificationFollow extends ImmutablePureComponent { static propTypes = { + hidden: PropTypes.bool, id: PropTypes.string.isRequired, account: ImmutablePropTypes.map.isRequired, notification: ImmutablePropTypes.map.isRequired, @@ -57,7 +58,7 @@ export default class NotificationFollow extends ImmutablePureComponent { } render () { - const { account, notification } = this.props; + const { account, notification, hidden } = this.props; // Links to the display name. const displayName = account.get('display_name_html') || account.get('username'); @@ -87,7 +88,7 @@ export default class NotificationFollow extends ImmutablePureComponent { />
- +
diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.js b/app/javascript/flavours/glitch/features/notifications/components/notification.js index 185da8395..cc77426d3 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.js +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.js @@ -16,70 +16,75 @@ export default class Notification extends ImmutablePureComponent { onMoveUp: PropTypes.func.isRequired, onMoveDown: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, - settings: ImmutablePropTypes.map.isRequired, }; - renderFollow () { - const { notification } = this.props; - return ( - - ); - } - - renderMention () { - const { notification } = this.props; - return ( - - ); - } - - renderFavourite () { - const { notification } = this.props; - return ( - - ); - } - - renderReblog () { - const { notification } = this.props; - return ( - - ); - } - render () { - const { notification } = this.props; + const { + hidden, + notification, + onMoveDown, + onMoveUp, + onMention, + } = this.props; + switch(notification.get('type')) { case 'follow': - return this.renderFollow(); + return ( +