From b6e2e999bd4c603bc36b1234af484184644104e9 Mon Sep 17 00:00:00 2001 From: nullkal Date: Tue, 7 Nov 2017 22:49:32 +0900 Subject: Show the local couterpart of emoji when it exists in /admin/custom_emojis (#5467) * Show the local couterpart of emoji when it exists in admin/custom_emojis * Fix indentation * Fix error * Add class table-action-link to Overwrite link * Make it enable to overwrite emojis * Make Code Climate happy --- app/models/custom_emoji.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app/models') diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 28b6a2b0b..a77b53c98 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -25,6 +25,8 @@ class CustomEmoji < ApplicationRecord :(#{SHORTCODE_RE_FRAGMENT}): (?=[^[:alnum:]:]|$)/x + has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode + has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce -strip' } } validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { in: 0..50.kilobytes } -- cgit From 1032f3994fdbd61c2f517057261ddc3559199b6b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 7 Nov 2017 19:06:44 +0100 Subject: Add ability to disable login and mark accounts as memorial (#5615) Fix #5597 --- app/controllers/admin/accounts_controller.rb | 22 ++++++++++++++- app/controllers/admin/suspensions_controller.rb | 2 +- app/javascript/styles/mastodon/landing_strip.scss | 7 ++++- app/mailers/notification_mailer.rb | 18 ++++++++---- app/mailers/user_mailer.rb | 6 ++++ app/models/account.rb | 15 ++++++++++ app/models/user.rb | 18 +++++++++++- app/services/suspend_account_service.rb | 25 +++++++++------- app/views/accounts/_header.html.haml | 33 +++++++++++----------- app/views/accounts/show.html.haml | 4 ++- app/views/admin/accounts/show.html.haml | 11 ++++++++ app/workers/admin/suspension_worker.rb | 2 +- config/locales/en.yml | 7 +++++ config/routes.rb | 3 ++ .../20171107143332_add_memorial_to_accounts.rb | 15 ++++++++++ db/migrate/20171107143624_add_disabled_to_users.rb | 15 ++++++++++ db/schema.rb | 4 ++- 17 files changed, 168 insertions(+), 39 deletions(-) create mode 100644 db/migrate/20171107143332_add_memorial_to_accounts.rb create mode 100644 db/migrate/20171107143624_add_disabled_to_users.rb (limited to 'app/models') diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index ffa4dc850..7503b880d 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -2,8 +2,9 @@ module Admin class AccountsController < BaseController - before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload] + before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :enable, :disable, :memorialize] before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] + before_action :require_local_account!, only: [:enable, :disable, :memorialize] def index @accounts = filtered_accounts.page(params[:page]) @@ -24,6 +25,21 @@ module Admin redirect_to admin_account_path(@account.id) end + def memorialize + @account.memorialize! + redirect_to admin_account_path(@account.id) + end + + def enable + @account.user.enable! + redirect_to admin_account_path(@account.id) + end + + def disable + @account.user.disable! + redirect_to admin_account_path(@account.id) + end + def redownload @account.reset_avatar! @account.reset_header! @@ -42,6 +58,10 @@ module Admin redirect_to admin_account_path(@account.id) if @account.local? end + def require_local_account! + redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present? + end + def filtered_accounts AccountFilter.new(filter_params).results end diff --git a/app/controllers/admin/suspensions_controller.rb b/app/controllers/admin/suspensions_controller.rb index 5d9048d94..5eaf1a2e9 100644 --- a/app/controllers/admin/suspensions_controller.rb +++ b/app/controllers/admin/suspensions_controller.rb @@ -10,7 +10,7 @@ module Admin end def destroy - @account.update(suspended: false) + @account.unsuspend! redirect_to admin_accounts_path end diff --git a/app/javascript/styles/mastodon/landing_strip.scss b/app/javascript/styles/mastodon/landing_strip.scss index 15ff84912..0bf9daafd 100644 --- a/app/javascript/styles/mastodon/landing_strip.scss +++ b/app/javascript/styles/mastodon/landing_strip.scss @@ -1,4 +1,5 @@ -.landing-strip { +.landing-strip, +.memoriam-strip { background: rgba(darken($ui-base-color, 7%), 0.8); color: $ui-primary-color; font-weight: 400; @@ -29,3 +30,7 @@ margin-bottom: 0; } } + +.memoriam-strip { + background: rgba($base-shadow-color, 0.7); +} diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 80c9d8ccf..d79f26366 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -7,6 +7,8 @@ class NotificationMailer < ApplicationMailer @me = recipient @status = notification.target_status + return if @me.user.disabled? + locale_for_account(@me) do thread_by_conversation(@status.conversation) mail to: @me.user.email, subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct) @@ -17,6 +19,8 @@ class NotificationMailer < ApplicationMailer @me = recipient @account = notification.from_account + return if @me.user.disabled? + locale_for_account(@me) do mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct) end @@ -27,6 +31,8 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status + return if @me.user.disabled? + locale_for_account(@me) do thread_by_conversation(@status.conversation) mail to: @me.user.email, subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct) @@ -38,6 +44,8 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status + return if @me.user.disabled? + locale_for_account(@me) do thread_by_conversation(@status.conversation) mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct) @@ -48,6 +56,8 @@ class NotificationMailer < ApplicationMailer @me = recipient @account = notification.from_account + return if @me.user.disabled? + locale_for_account(@me) do mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct) end @@ -59,15 +69,11 @@ class NotificationMailer < ApplicationMailer @notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since) @follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count - return if @notifications.empty? + return if @me.user.disabled? || @notifications.empty? locale_for_account(@me) do mail to: @me.user.email, - subject: I18n.t( - :subject, - scope: [:notification_mailer, :digest], - count: @notifications.size - ) + subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications.size) end end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index c475a9911..bdb29ebad 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -10,6 +10,8 @@ class UserMailer < Devise::Mailer @token = token @instance = Rails.configuration.x.local_domain + return if @resource.disabled? + I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.unconfirmed_email.blank? ? @resource.email : @resource.unconfirmed_email, subject: I18n.t('devise.mailer.confirmation_instructions.subject', instance: @instance) end @@ -20,6 +22,8 @@ class UserMailer < Devise::Mailer @token = token @instance = Rails.configuration.x.local_domain + return if @resource.disabled? + I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject') end @@ -29,6 +33,8 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain + return if @resource.disabled? + I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject') end diff --git a/app/models/account.rb b/app/models/account.rb index 3dc2a95ab..1142e7c79 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -41,6 +41,7 @@ # shared_inbox_url :string default(""), not null # followers_url :string default(""), not null # protocol :integer default("ostatus"), not null +# memorial :boolean default(FALSE), not null # class Account < ApplicationRecord @@ -150,6 +151,20 @@ class Account < ApplicationRecord ResolveRemoteAccountService.new.call(acct) end + def unsuspend! + transaction do + user&.enable! if local? + update!(suspended: false) + end + end + + def memorialize! + transaction do + user&.disable! if local? + update!(memorial: true) + end + end + def keypair @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key) end diff --git a/app/models/user.rb b/app/models/user.rb index 325e27f44..836d54d15 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,7 +5,6 @@ # # id :integer not null, primary key # email :string default(""), not null -# account_id :integer not null # created_at :datetime not null # updated_at :datetime not null # encrypted_password :string default(""), not null @@ -31,10 +30,13 @@ # last_emailed_at :datetime # otp_backup_codes :string is an Array # filtered_languages :string default([]), not null, is an Array +# account_id :integer not null +# disabled :boolean default(FALSE), not null # class User < ApplicationRecord include Settings::Extend + ACTIVE_DURATION = 14.days devise :registerable, :recoverable, @@ -72,12 +74,26 @@ class User < ApplicationRecord confirmed_at.present? end + def disable! + update!(disabled: true, + last_sign_in_at: current_sign_in_at, + current_sign_in_at: nil) + end + + def enable! + update!(disabled: false) + end + def disable_two_factor! self.otp_required_for_login = false otp_backup_codes&.clear save! end + def active_for_authentication? + super && !disabled? + end + def setting_default_privacy settings.default_privacy || (account.locked? ? 'private' : 'public') end diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 983c5495b..5b37ba9ba 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -1,22 +1,27 @@ # frozen_string_literal: true class SuspendAccountService < BaseService - def call(account, remove_user = false) + def call(account, options = {}) @account = account + @options = options - purge_user if remove_user - purge_profile - purge_content - unsubscribe_push_subscribers + purge_user! + purge_profile! + purge_content! + unsubscribe_push_subscribers! end private - def purge_user - @account.user.destroy + def purge_user! + if @options[:remove_user] + @account.user&.destroy + else + @account.user&.disable! + end end - def purge_content + def purge_content! @account.statuses.reorder(nil).find_in_batches do |statuses| BatchedRemoveStatusService.new.call(statuses) end @@ -33,7 +38,7 @@ class SuspendAccountService < BaseService end end - def purge_profile + def purge_profile! @account.suspended = true @account.display_name = '' @account.note = '' @@ -42,7 +47,7 @@ class SuspendAccountService < BaseService @account.save! end - def unsubscribe_push_subscribers + def unsubscribe_push_subscribers! destroy_all(@account.subscriptions) end diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index 08c3891d2..5530fcc20 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -1,21 +1,22 @@ .card.h-card.p-author{ style: "background-image: url(#{account.header.url(:original)})" } .card__illustration - - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account) - .controls - - if current_account.following?(account) - = link_to account_unfollow_path(account), data: { method: :post }, class: 'icon-button' do - = fa_icon 'user-times' - = t('accounts.unfollow') - - else - = link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do - = fa_icon 'user-plus' - = t('accounts.follow') - - elsif !user_signed_in? - .controls - .remote-follow - = link_to account_remote_follow_path(account), class: 'icon-button' do - = fa_icon 'user-plus' - = t('accounts.remote_follow') + - unless account.memorial? + - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account) + .controls + - if current_account.following?(account) + = link_to account_unfollow_path(account), data: { method: :post }, class: 'icon-button' do + = fa_icon 'user-times' + = t('accounts.unfollow') + - else + = link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do + = fa_icon 'user-plus' + = t('accounts.follow') + - elsif !user_signed_in? + .controls + .remote-follow + = link_to account_remote_follow_path(account), class: 'icon-button' do + = fa_icon 'user-plus' + = t('accounts.remote_follow') .avatar= image_tag account.avatar.url(:original), class: 'u-photo' diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 6c90b2c04..fd8ad5530 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -12,7 +12,9 @@ = opengraph 'og:type', 'profile' = render 'og', account: @account, url: short_account_url(@account, only_path: false) -- if show_landing_strip? +- if @account.memorial? + .memoriam-strip= t('in_memoriam_html') +- elsif show_landing_strip? = render partial: 'shared/landing_strip', locals: { account: @account } .h-feed diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 1f5c8fcf5..b5ce56dbc 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -18,6 +18,15 @@ %tr %th= t('admin.accounts.email') %td= @account.user_email + %tr + %th= t('admin.accounts.login_status') + %td + - if @account.user&.disabled? + = t('admin.accounts.disabled') + = table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post + - else + = t('admin.accounts.enabled') + = table_link_to 'lock', t('admin.accounts.disable'), disable_admin_account_path(@account.id), method: :post %tr %th= t('admin.accounts.most_recent_ip') %td= @account.user_current_sign_in_ip @@ -65,6 +74,8 @@ = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' - if @account.user&.otp_required_for_login? = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' + - unless @account.memorial? + = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' - else = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' diff --git a/app/workers/admin/suspension_worker.rb b/app/workers/admin/suspension_worker.rb index 6338b1130..e41465ccc 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), remove_user) + SuspendAccountService.new.call(Account.find(account_id), remove_user: remove_user) end end diff --git a/config/locales/en.yml b/config/locales/en.yml index ce439029c..41a636760 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -62,11 +62,15 @@ en: by_domain: Domain confirm: Confirm confirmed: Confirmed + disable: Disable disable_two_factor_authentication: Disable 2FA + disabled: Disabled display_name: Display name domain: Domain edit: Edit email: E-mail + enable: Enable + enabled: Enabled feed_url: Feed URL followers: Followers followers_url: Followers URL @@ -78,7 +82,9 @@ en: local: Local remote: Remote title: Location + login_status: Login status media_attachments: Media attachments + memorialize: Turn into memoriam moderation: all: All silenced: Silenced @@ -379,6 +385,7 @@ en: following: Following list muting: Muting list upload: Upload + in_memoriam_html: In Memoriam. landing_strip_html: "%{name} is a user on %{link_to_root_path}. You can follow them or interact with them if you have an account anywhere in the fediverse." landing_strip_signup_html: If you don't, you can sign up here. media_attachments: diff --git a/config/routes.rb b/config/routes.rb index bdfcdaff6..e6d6b52f7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -126,7 +126,10 @@ Rails.application.routes.draw do member do post :subscribe post :unsubscribe + post :enable + post :disable post :redownload + post :memorialize end resource :reset, only: [:create] diff --git a/db/migrate/20171107143332_add_memorial_to_accounts.rb b/db/migrate/20171107143332_add_memorial_to_accounts.rb new file mode 100644 index 000000000..f3e012ce8 --- /dev/null +++ b/db/migrate/20171107143332_add_memorial_to_accounts.rb @@ -0,0 +1,15 @@ +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddMemorialToAccounts < ActiveRecord::Migration[5.1] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def up + safety_assured { add_column_with_default :accounts, :memorial, :bool, default: false } + end + + def down + remove_column :accounts, :memorial + end +end diff --git a/db/migrate/20171107143624_add_disabled_to_users.rb b/db/migrate/20171107143624_add_disabled_to_users.rb new file mode 100644 index 000000000..a71cac1c6 --- /dev/null +++ b/db/migrate/20171107143624_add_disabled_to_users.rb @@ -0,0 +1,15 @@ +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddDisabledToUsers < ActiveRecord::Migration[5.1] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def up + safety_assured { add_column_with_default :users, :disabled, :bool, default: false } + end + + def down + remove_column :users, :disabled + end +end diff --git a/db/schema.rb b/db/schema.rb index 697a7f374..935fd79c5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171020084748) do +ActiveRecord::Schema.define(version: 20171107143624) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -71,6 +71,7 @@ ActiveRecord::Schema.define(version: 20171020084748) do t.string "shared_inbox_url", default: "", null: false t.string "followers_url", default: "", null: false t.integer "protocol", default: 0, null: false + t.boolean "memorial", default: false, null: false t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower" t.index ["uri"], name: "index_accounts_on_uri" @@ -435,6 +436,7 @@ ActiveRecord::Schema.define(version: 20171020084748) do t.string "otp_backup_codes", array: true t.string "filtered_languages", default: [], null: false, array: true t.bigint "account_id", null: false + t.boolean "disabled", default: false, null: false t.index ["account_id"], name: "index_users_on_account_id" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true -- cgit From 5d5c0f4f4358f4349d9e2db59cf90b1f5de24e81 Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 7 Nov 2017 19:08:14 +0100 Subject: Twidere mention workaround (#5552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Work around Twidere and Tootdon bug Tootdon and Twidere construct @user@domain handles from mentions in toots based solely on the mention text and account URI's domain without performing any webfinger call or retrieving account info from the Mastodon server. As a result, when a remote user has WEB_DOMAIN ≠ LOCAL_DOMAIN, Twidere and Tootdon will construct the mention as @user@WEB_DOMAIN. Now, this will usually resolve to the correct account (since the recommended configuration is to have WEB_DOMAIN perform webfinger redirections to LOCAL_DOMAIN) when processing mentions, but won't do so when displaying them (as it does not go through the whole account resolution at that time). This change rewrites mentions to the resolved account, so that displaying the mentions will work. * Use lookbehind instead of non-capturing group in MENTION_RE Indeed, substitutions with the previous regexp would erroneously eat any preceding whitespace, which would lead to concatenated mentions in the previous commit. Note that users will “lose” up to one character space per mention for their toots, as that regexp is also used to remove the domain-part of mentioned users for character counting purposes, and it also erroneously removed the preceding character if it was a space. --- app/javascript/mastodon/features/compose/util/counter.js | 2 +- app/models/account.rb | 2 +- app/services/process_mentions_service.rb | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) (limited to 'app/models') diff --git a/app/javascript/mastodon/features/compose/util/counter.js b/app/javascript/mastodon/features/compose/util/counter.js index e6d2487c5..700ba2163 100644 --- a/app/javascript/mastodon/features/compose/util/counter.js +++ b/app/javascript/mastodon/features/compose/util/counter.js @@ -5,5 +5,5 @@ const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx'; export function countableText(inputText) { return inputText .replace(urlRegex, urlPlaceholder) - .replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '@$2'); + .replace(/(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '$1@$3'); }; diff --git a/app/models/account.rb b/app/models/account.rb index 1142e7c79..7e4d29f96 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -45,7 +45,7 @@ # class Account < ApplicationRecord - MENTION_RE = /(?:^|[^\/[:word:]])@(([a-z0-9_]+)(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i + MENTION_RE = /(?<=^|[^\/[:word:]])@(([a-z0-9_]+)(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i include AccountAvatar include AccountFinderConcern diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index aa649652c..65e6b1361 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -10,18 +10,21 @@ class ProcessMentionsService < BaseService def call(status) return unless status.local? - status.text.scan(Account::MENTION_RE).each do |match| + status.text = status.text.gsub(Account::MENTION_RE) do |match| begin - mentioned_account = resolve_remote_account_service.call(match.first.to_s) + mentioned_account = resolve_remote_account_service.call($1) rescue Goldfinger::Error, HTTP::Error mentioned_account = nil end - next if mentioned_account.nil? || (mentioned_account.ostatus? && status.stream_entry.hidden?) + next match if mentioned_account.nil? || (mentioned_account.ostatus? && status.stream_entry.hidden?) mentioned_account.mentions.where(status: status).first_or_create(status: status) + "@#{mentioned_account.acct}" end + status.save! + status.mentions.includes(:account).each do |mention| create_notification(status, mention) end -- cgit From 54b42901df938257ae660168958fffe58b1a0287 Mon Sep 17 00:00:00 2001 From: ysksn Date: Thu, 9 Nov 2017 22:36:52 +0900 Subject: Add and Remove tests for FollowRequest (#5622) * Add a test for FollowRequest#authorize! * Remove tests There is no need to test ActiveModel::Validations::ClassMethods#validates. * Make an alias of destroy! as reject! Instead of defining the method, make an alias of destroy! as reject! because of reducing test. --- app/models/follow_request.rb | 4 +--- spec/models/follow_request_spec.rb | 27 +++++++++------------------ 2 files changed, 10 insertions(+), 21 deletions(-) (limited to 'app/models') diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 60036d903..458bcd28a 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -27,7 +27,5 @@ class FollowRequest < ApplicationRecord destroy! end - def reject! - destroy! - end + alias reject! destroy! end diff --git a/spec/models/follow_request_spec.rb b/spec/models/follow_request_spec.rb index cc6f8ee62..1436501e9 100644 --- a/spec/models/follow_request_spec.rb +++ b/spec/models/follow_request_spec.rb @@ -1,25 +1,16 @@ require 'rails_helper' RSpec.describe FollowRequest, type: :model do - describe '#authorize!' - describe '#reject!' + describe '#authorize!' do + let(:follow_request) { Fabricate(:follow_request, account: account, target_account: target_account) } + let(:account) { Fabricate(:account) } + let(:target_account) { Fabricate(:account) } - describe 'validations' do - it 'has a valid fabricator' do - follow_request = Fabricate.build(:follow_request) - expect(follow_request).to be_valid - end - - it 'is invalid without an account' do - follow_request = Fabricate.build(:follow_request, account: nil) - follow_request.valid? - expect(follow_request).to model_have_error_on_field(:account) - end - - it 'is invalid without a target account' do - follow_request = Fabricate.build(:follow_request, target_account: nil) - follow_request.valid? - expect(follow_request).to model_have_error_on_field(:target_account) + it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do + expect(account).to receive(:follow!).with(target_account) + expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id) + expect(follow_request).to receive(:destroy!) + follow_request.authorize! end end end -- cgit From 7bb8b0b2fc0e2e42a4234fed18198cbb7439fe9f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 11 Nov 2017 20:23:33 +0100 Subject: Add moderator role and add pundit policies for admin actions (#5635) * Add moderator role and add pundit policies for admin actions * Add rake task for turning user into mod and revoking it again * Fix handling of unauthorized exception * Deliver new report e-mails to staff, not just admins * Add promote/demote to admin UI, hide some actions conditionally * Fix unused i18n --- .../admin/account_moderation_notes_controller.rb | 56 +++++++++++++--------- app/controllers/admin/accounts_controller.rb | 9 ++++ app/controllers/admin/base_controller.rb | 4 +- app/controllers/admin/confirmations_controller.rb | 9 ++-- app/controllers/admin/custom_emojis_controller.rb | 11 +++++ app/controllers/admin/domain_blocks_controller.rb | 9 +++- .../admin/email_domain_blocks_controller.rb | 5 ++ app/controllers/admin/instances_controller.rb | 2 + .../admin/reported_statuses_controller.rb | 9 ++-- app/controllers/admin/reports_controller.rb | 3 ++ app/controllers/admin/resets_controller.rb | 9 ++-- app/controllers/admin/roles_controller.rb | 25 ++++++++++ app/controllers/admin/settings_controller.rb | 3 ++ app/controllers/admin/silences_controller.rb | 2 + app/controllers/admin/statuses_controller.rb | 17 ++++--- app/controllers/admin/subscriptions_controller.rb | 1 + app/controllers/admin/suspensions_controller.rb | 2 + .../admin/two_factor_authentications_controller.rb | 1 + app/controllers/api/v1/reports_controller.rb | 2 +- app/controllers/application_controller.rb | 5 ++ app/controllers/concerns/authorization.rb | 1 + app/helpers/application_helper.rb | 5 ++ app/models/user.rb | 42 +++++++++++++++- app/policies/account_moderation_note_policy.rb | 17 +++++++ app/policies/account_policy.rb | 43 +++++++++++++++++ app/policies/application_policy.rb | 18 +++++++ app/policies/custom_emoji_policy.rb | 31 ++++++++++++ app/policies/domain_block_policy.rb | 19 ++++++++ app/policies/email_domain_block_policy.rb | 15 ++++++ app/policies/instance_policy.rb | 11 +++++ app/policies/report_policy.rb | 15 ++++++ app/policies/settings_policy.rb | 11 +++++ app/policies/status_policy.rb | 35 +++++++------- app/policies/subscription_policy.rb | 7 +++ app/policies/user_policy.rb | 41 ++++++++++++++++ .../_account_moderation_note.html.haml | 2 +- app/views/admin/accounts/show.html.haml | 46 ++++++++++++------ config/i18n-tasks.yml | 2 + config/locales/en.yml | 7 +++ config/navigation.rb | 16 +++---- config/routes.rb | 7 +++ .../20171109012327_add_moderator_to_accounts.rb | 15 ++++++ db/schema.rb | 3 +- lib/tasks/mastodon.rake | 31 +++++++++++- 44 files changed, 536 insertions(+), 88 deletions(-) create mode 100644 app/controllers/admin/roles_controller.rb create mode 100644 app/policies/account_moderation_note_policy.rb create mode 100644 app/policies/account_policy.rb create mode 100644 app/policies/application_policy.rb create mode 100644 app/policies/custom_emoji_policy.rb create mode 100644 app/policies/domain_block_policy.rb create mode 100644 app/policies/email_domain_block_policy.rb create mode 100644 app/policies/instance_policy.rb create mode 100644 app/policies/report_policy.rb create mode 100644 app/policies/settings_policy.rb create mode 100644 app/policies/subscription_policy.rb create mode 100644 app/policies/user_policy.rb create mode 100644 db/migrate/20171109012327_add_moderator_to_accounts.rb (limited to 'app/models') diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb index 414a875d0..7f69a3363 100644 --- a/app/controllers/admin/account_moderation_notes_controller.rb +++ b/app/controllers/admin/account_moderation_notes_controller.rb @@ -1,31 +1,41 @@ # frozen_string_literal: true -class Admin::AccountModerationNotesController < Admin::BaseController - def create - @account_moderation_note = current_account.account_moderation_notes.new(resource_params) - if @account_moderation_note.save - @target_account = @account_moderation_note.target_account - redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.created_msg') - else - @account = @account_moderation_note.target_account - @moderation_notes = @account.targeted_moderation_notes.latest - render template: 'admin/accounts/show' +module Admin + class AccountModerationNotesController < BaseController + before_action :set_account_moderation_note, only: [:destroy] + + def create + authorize AccountModerationNote, :create? + + @account_moderation_note = current_account.account_moderation_notes.new(resource_params) + + if @account_moderation_note.save + redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg') + else + @account = @account_moderation_note.target_account + @moderation_notes = @account.targeted_moderation_notes.latest + + render template: 'admin/accounts/show' + end end - end - def destroy - @account_moderation_note = AccountModerationNote.find(params[:id]) - @target_account = @account_moderation_note.target_account - @account_moderation_note.destroy - redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg') - end + def destroy + authorize @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 - private + private - def resource_params - params.require(:account_moderation_note).permit( - :content, - :target_account_id - ) + def resource_params + params.require(:account_moderation_note).permit( + :content, + :target_account_id + ) + end + + def set_account_moderation_note + @account_moderation_note = AccountModerationNote.find(params[:id]) + end end end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 7503b880d..0829bc769 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -7,40 +7,49 @@ module Admin before_action :require_local_account!, only: [:enable, :disable, :memorialize] def index + authorize :account, :index? @accounts = filtered_accounts.page(params[:page]) end def show + authorize @account, :show? @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account) @moderation_notes = @account.targeted_moderation_notes.latest end def subscribe + authorize @account, :subscribe? Pubsubhubbub::SubscribeWorker.perform_async(@account.id) redirect_to admin_account_path(@account.id) end def unsubscribe + authorize @account, :unsubscribe? Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id) redirect_to admin_account_path(@account.id) end def memorialize + authorize @account, :memorialize? @account.memorialize! redirect_to admin_account_path(@account.id) end def enable + authorize @account.user, :enable? @account.user.enable! redirect_to admin_account_path(@account.id) end def disable + authorize @account.user, :disable? @account.user.disable! redirect_to admin_account_path(@account.id) end def redownload + authorize @account, :redownload? + @account.reset_avatar! @account.reset_header! @account.save! diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 11fe326bc..db4839a8f 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -2,7 +2,9 @@ module Admin class BaseController < ApplicationController - before_action :require_admin! + include Authorization + + before_action :require_staff! layout 'admin' end diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index 2542e21ee..c10b0ebee 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -2,15 +2,18 @@ module Admin class ConfirmationsController < BaseController + before_action :set_user + def create - account_user.confirm + authorize @user, :confirm? + @user.confirm! redirect_to admin_accounts_path end private - def account_user - Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) + def set_user + @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) end end end diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index daa1460fb..693d28b1f 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -5,14 +5,18 @@ module Admin before_action :set_custom_emoji, except: [:index, :new, :create] def index + authorize :custom_emoji, :index? @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) end def new + authorize :custom_emoji, :create? @custom_emoji = CustomEmoji.new end def create + authorize :custom_emoji, :create? + @custom_emoji = CustomEmoji.new(resource_params) if @custom_emoji.save @@ -23,6 +27,8 @@ module Admin end def update + authorize @custom_emoji, :update? + if @custom_emoji.update(resource_params) redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg') else @@ -31,11 +37,14 @@ module Admin end def destroy + authorize @custom_emoji, :destroy? @custom_emoji.destroy redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg') end def copy + authorize @custom_emoji, :copy? + emoji = CustomEmoji.find_or_create_by(domain: nil, shortcode: @custom_emoji.shortcode) if emoji.update(image: @custom_emoji.image) @@ -48,11 +57,13 @@ module Admin end def enable + authorize @custom_emoji, :enable? @custom_emoji.update!(disabled: false) 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) 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 1ab620e03..e383dc831 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -5,14 +5,18 @@ module Admin before_action :set_domain_block, only: [:show, :destroy] def index + authorize :domain_block, :index? @domain_blocks = DomainBlock.page(params[:page]) end def new + authorize :domain_block, :create? @domain_block = DomainBlock.new end def create + authorize :domain_block, :create? + @domain_block = DomainBlock.new(resource_params) if @domain_block.save @@ -23,9 +27,12 @@ module Admin end end - def show; end + def show + authorize @domain_block, :show? + end def destroy + authorize @domain_block, :destroy? UnblockDomainService.new.call(@domain_block, retroactive_unblock?) 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 09275d5dc..01058bf46 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -5,14 +5,18 @@ module Admin before_action :set_email_domain_block, only: [:show, :destroy] def index + authorize :email_domain_block, :index? @email_domain_blocks = EmailDomainBlock.page(params[:page]) end def new + authorize :email_domain_block, :create? @email_domain_block = EmailDomainBlock.new end def create + authorize :email_domain_block, :create? + @email_domain_block = EmailDomainBlock.new(resource_params) if @email_domain_block.save @@ -23,6 +27,7 @@ module Admin end def destroy + authorize @email_domain_block, :destroy? @email_domain_block.destroy redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg') end diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 22f02e5d0..8ed0ea421 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -3,10 +3,12 @@ module Admin class InstancesController < BaseController def index + authorize :instance, :index? @instances = ordered_instances end def resubscribe + authorize :instance, :resubscribe? params.require(:by_domain) Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id)) redirect_to admin_instances_path diff --git a/app/controllers/admin/reported_statuses_controller.rb b/app/controllers/admin/reported_statuses_controller.rb index 5a31adecf..4f66ce708 100644 --- a/app/controllers/admin/reported_statuses_controller.rb +++ b/app/controllers/admin/reported_statuses_controller.rb @@ -2,19 +2,20 @@ module Admin class ReportedStatusesController < BaseController - include Authorization - before_action :set_report before_action :set_status, only: [:update, :destroy] def create - @form = Form::StatusBatch.new(form_status_batch_params) - flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save + authorize :status, :update? + + @form = Form::StatusBatch.new(form_status_batch_params) + flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save redirect_to admin_report_path(@report) end def update + authorize @status, :update? @status.update(status_params) redirect_to admin_report_path(@report) end diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 226467739..745757ee8 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -5,14 +5,17 @@ module Admin before_action :set_report, except: [:index] def index + authorize :report, :index? @reports = filtered_reports.page(params[:page]) end def show + authorize @report, :show? @form = Form::StatusBatch.new end def update + authorize @report, :update? process_report redirect_to admin_report_path(@report) end diff --git a/app/controllers/admin/resets_controller.rb b/app/controllers/admin/resets_controller.rb index 6db648403..00b590bf6 100644 --- a/app/controllers/admin/resets_controller.rb +++ b/app/controllers/admin/resets_controller.rb @@ -2,17 +2,18 @@ module Admin class ResetsController < BaseController - before_action :set_account + before_action :set_user def create - @account.user.send_reset_password_instructions + authorize @user, :reset_password? + @user.send_reset_password_instructions redirect_to admin_accounts_path end private - def set_account - @account = Account.find(params[:account_id]) + def set_user + @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) end end end diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb new file mode 100644 index 000000000..8f8685827 --- /dev/null +++ b/app/controllers/admin/roles_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Admin + class RolesController < BaseController + before_action :set_user + + def promote + authorize @user, :promote? + @user.promote! + redirect_to admin_account_path(@user.account_id) + end + + def demote + authorize @user, :demote? + @user.demote! + redirect_to admin_account_path(@user.account_id) + end + + private + + def set_user + @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) + end + end +end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index a2f86b8a9..e81290228 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -28,10 +28,13 @@ module Admin ).freeze def edit + authorize :settings, :show? @admin_settings = Form::AdminSettings.new end def update + authorize :settings, :update? + settings_params.each do |key, value| if UPLOAD_SETTINGS.include?(key) upload = SiteUpload.where(var: key).first_or_initialize(var: key) diff --git a/app/controllers/admin/silences_controller.rb b/app/controllers/admin/silences_controller.rb index 81a3008b9..01fb292de 100644 --- a/app/controllers/admin/silences_controller.rb +++ b/app/controllers/admin/silences_controller.rb @@ -5,11 +5,13 @@ module Admin before_action :set_account def create + authorize @account, :silence? @account.update(silenced: true) redirect_to admin_accounts_path end def destroy + authorize @account, :unsilence? @account.update(silenced: false) redirect_to admin_accounts_path end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index b05000b16..b54a9b824 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -2,8 +2,6 @@ module Admin class StatusesController < BaseController - include Authorization - helper_method :current_params before_action :set_account @@ -12,24 +10,30 @@ module Admin PER_PAGE = 20 def index + authorize :status, :index? + @statuses = @account.statuses + if params[:media] account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct @statuses.merge!(Status.where(id: account_media_status_ids)) end - @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE) - @form = Form::StatusBatch.new + @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE) + @form = Form::StatusBatch.new end def create - @form = Form::StatusBatch.new(form_status_batch_params) - flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save + authorize :status, :update? + + @form = Form::StatusBatch.new(form_status_batch_params) + flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save redirect_to admin_account_statuses_path(@account.id, current_params) end def update + authorize @status, :update? @status.update(status_params) redirect_to admin_account_statuses_path(@account.id, current_params) end @@ -60,6 +64,7 @@ module Admin def current_params page = (params[:page] || 1).to_i + { media: params[:media], page: page > 1 && page, diff --git a/app/controllers/admin/subscriptions_controller.rb b/app/controllers/admin/subscriptions_controller.rb index 624a475a3..40500ef43 100644 --- a/app/controllers/admin/subscriptions_controller.rb +++ b/app/controllers/admin/subscriptions_controller.rb @@ -3,6 +3,7 @@ module Admin class SubscriptionsController < BaseController def index + authorize :subscription, :index? @subscriptions = ordered_subscriptions.page(requested_page) end diff --git a/app/controllers/admin/suspensions_controller.rb b/app/controllers/admin/suspensions_controller.rb index 5eaf1a2e9..778feea5e 100644 --- a/app/controllers/admin/suspensions_controller.rb +++ b/app/controllers/admin/suspensions_controller.rb @@ -5,11 +5,13 @@ module Admin before_action :set_account def create + authorize @account, :suspend? Admin::SuspensionWorker.perform_async(@account.id) redirect_to admin_accounts_path end def destroy + authorize @account, :unsuspend? @account.unsuspend! 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 69c08f605..5a45d25cd 100644 --- a/app/controllers/admin/two_factor_authentications_controller.rb +++ b/app/controllers/admin/two_factor_authentications_controller.rb @@ -5,6 +5,7 @@ module Admin before_action :set_user def destroy + authorize @user, :disable_2fa? @user.disable_two_factor! redirect_to admin_accounts_path end diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 9592cd4bd..22828217d 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -19,7 +19,7 @@ class Api::V1::ReportsController < Api::BaseController comment: report_params[:comment] ) - User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later } + User.staff.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later } render json: @report, serializer: REST::ReportSerializer end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d5eca6ffb..f41a7f9be 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -18,6 +18,7 @@ class ApplicationController < ActionController::Base rescue_from ActionController::RoutingError, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity + rescue_from Mastodon::NotPermittedError, with: :forbidden before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :check_suspension, if: :user_signed_in? @@ -40,6 +41,10 @@ class ApplicationController < ActionController::Base redirect_to root_path unless current_user&.admin? end + def require_staff! + redirect_to root_path unless current_user&.staff? + end + def check_suspension forbidden if current_user.account.suspended? end diff --git a/app/controllers/concerns/authorization.rb b/app/controllers/concerns/authorization.rb index 7828fe48d..95a37e379 100644 --- a/app/controllers/concerns/authorization.rb +++ b/app/controllers/concerns/authorization.rb @@ -2,6 +2,7 @@ module Authorization extend ActiveSupport::Concern + include Pundit def pundit_user diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 310e1b1b1..7dfab1df1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -35,6 +35,11 @@ module ApplicationHelper Rails.env.production? ? site_title : "#{site_title} (Dev)" end + def can?(action, record) + return false if record.nil? + policy(record).public_send("#{action}?") + end + def fa_icon(icon, attributes = {}) class_names = attributes[:class]&.split(' ') || [] class_names << 'fa' diff --git a/app/models/user.rb b/app/models/user.rb index 836d54d15..9022e6ea8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -32,6 +32,7 @@ # filtered_languages :string default([]), not null, is an Array # account_id :integer not null # disabled :boolean default(FALSE), not null +# moderator :boolean default(FALSE), not null # class User < ApplicationRecord @@ -53,8 +54,10 @@ class User < ApplicationRecord validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? validates_with BlacklistedEmailValidator, if: :email_changed? - scope :recent, -> { order(id: :desc) } - scope :admins, -> { where(admin: true) } + scope :recent, -> { order(id: :desc) } + scope :admins, -> { where(admin: true) } + scope :moderators, -> { where(moderator: true) } + scope :staff, -> { admins.or(moderators) } scope :confirmed, -> { where.not(confirmed_at: nil) } scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) } scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended: false }) } @@ -74,6 +77,20 @@ class User < ApplicationRecord confirmed_at.present? end + def staff? + admin? || moderator? + end + + def role + if admin? + 'admin' + elsif moderator? + 'moderator' + else + 'user' + end + end + def disable! update!(disabled: true, last_sign_in_at: current_sign_in_at, @@ -84,6 +101,27 @@ class User < ApplicationRecord update!(disabled: false) end + def confirm! + skip_confirmation! + save! + end + + def promote! + if moderator? + update!(moderator: false, admin: true) + elsif !admin? + update!(moderator: true) + end + end + + def demote! + if admin? + update!(admin: false, moderator: true) + elsif moderator? + update!(moderator: false) + end + end + def disable_two_factor! self.otp_required_for_login = false otp_backup_codes&.clear diff --git a/app/policies/account_moderation_note_policy.rb b/app/policies/account_moderation_note_policy.rb new file mode 100644 index 000000000..885411a5b --- /dev/null +++ b/app/policies/account_moderation_note_policy.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AccountModerationNotePolicy < ApplicationPolicy + def create? + staff? + end + + def destroy? + admin? || owner? + end + + private + + def owner? + record.account_id == current_account&.id + end +end diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb new file mode 100644 index 000000000..85e2c8419 --- /dev/null +++ b/app/policies/account_policy.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class AccountPolicy < ApplicationPolicy + def index? + staff? + end + + def show? + staff? + end + + def suspend? + staff? && !record.user&.staff? + end + + def unsuspend? + staff? + end + + def silence? + staff? && !record.user&.staff? + end + + def unsilence? + staff? + end + + def redownload? + admin? + end + + def subscribe? + admin? + end + + def unsubscribe? + admin? + end + + def memorialize? + admin? && !record.user&.admin? + end +end diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb new file mode 100644 index 000000000..3e617001f --- /dev/null +++ b/app/policies/application_policy.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class ApplicationPolicy + attr_reader :current_account, :record + + def initialize(current_account, record) + @current_account = current_account + @record = record + end + + delegate :admin?, :moderator?, :staff?, to: :current_user, allow_nil: true + + private + + def current_user + current_account&.user + end +end diff --git a/app/policies/custom_emoji_policy.rb b/app/policies/custom_emoji_policy.rb new file mode 100644 index 000000000..a8c3cbc73 --- /dev/null +++ b/app/policies/custom_emoji_policy.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class CustomEmojiPolicy < ApplicationPolicy + def index? + staff? + end + + def create? + admin? + end + + def update? + admin? + end + + def copy? + admin? + end + + def enable? + staff? + end + + def disable? + staff? + end + + def destroy? + admin? + end +end diff --git a/app/policies/domain_block_policy.rb b/app/policies/domain_block_policy.rb new file mode 100644 index 000000000..47c0a81af --- /dev/null +++ b/app/policies/domain_block_policy.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class DomainBlockPolicy < ApplicationPolicy + def index? + admin? + end + + def show? + admin? + end + + def create? + admin? + end + + def destroy? + admin? + end +end diff --git a/app/policies/email_domain_block_policy.rb b/app/policies/email_domain_block_policy.rb new file mode 100644 index 000000000..5a75ee183 --- /dev/null +++ b/app/policies/email_domain_block_policy.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class EmailDomainBlockPolicy < ApplicationPolicy + def index? + admin? + end + + def create? + admin? + end + + def destroy? + admin? + end +end diff --git a/app/policies/instance_policy.rb b/app/policies/instance_policy.rb new file mode 100644 index 000000000..d1956e2de --- /dev/null +++ b/app/policies/instance_policy.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class InstancePolicy < ApplicationPolicy + def index? + admin? + end + + def resubscribe? + admin? + end +end diff --git a/app/policies/report_policy.rb b/app/policies/report_policy.rb new file mode 100644 index 000000000..95b5c30c8 --- /dev/null +++ b/app/policies/report_policy.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ReportPolicy < ApplicationPolicy + def update? + staff? + end + + def index? + staff? + end + + def show? + staff? + end +end diff --git a/app/policies/settings_policy.rb b/app/policies/settings_policy.rb new file mode 100644 index 000000000..2dcb79f51 --- /dev/null +++ b/app/policies/settings_policy.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class SettingsPolicy < ApplicationPolicy + def update? + admin? + end + + def show? + admin? + end +end diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 2ded61850..0373fdf04 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -1,20 +1,17 @@ # frozen_string_literal: true -class StatusPolicy - attr_reader :account, :status - - def initialize(account, status) - @account = account - @status = status +class StatusPolicy < ApplicationPolicy + def index? + staff? end def show? if direct? - owned? || status.mentions.where(account: account).exists? + owned? || record.mentions.where(account: current_account).exists? elsif private? - owned? || account&.following?(status.account) || status.mentions.where(account: account).exists? + owned? || current_account&.following?(author) || record.mentions.where(account: current_account).exists? else - account.nil? || !status.account.blocking?(account) + current_account.nil? || !author.blocking?(current_account) end end @@ -23,26 +20,30 @@ class StatusPolicy end def destroy? - admin? || owned? + staff? || owned? end alias unreblog? destroy? - private - - def admin? - account&.user&.admin? + def update? + staff? end + private + def direct? - status.direct_visibility? + record.direct_visibility? end def owned? - status.account.id == account&.id + author.id == current_account&.id end def private? - status.private_visibility? + record.private_visibility? + end + + def author + record.account end end diff --git a/app/policies/subscription_policy.rb b/app/policies/subscription_policy.rb new file mode 100644 index 000000000..ac9a8a6c4 --- /dev/null +++ b/app/policies/subscription_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class SubscriptionPolicy < ApplicationPolicy + def index? + admin? + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 000000000..aae207d06 --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class UserPolicy < ApplicationPolicy + def reset_password? + staff? && !record.staff? + end + + def disable_2fa? + admin? && !record.staff? + end + + def confirm? + staff? && !record.confirmed? + end + + def enable? + admin? + end + + def disable? + admin? && !record.admin? + end + + def promote? + admin? && promoteable? + end + + def demote? + admin? && !record.admin? && demoteable? + end + + private + + def promoteable? + !record.staff? || !record.admin? + end + + def demoteable? + record.staff? + end +end diff --git a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml b/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml index 4651630e9..6761a4319 100644 --- a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml +++ b/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml @@ -7,4 +7,4 @@ %time.formatted{ datetime: account_moderation_note.created_at.iso8601, title: l(account_moderation_note.created_at) } = l account_moderation_note.created_at %td - = link_to t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete + = link_to t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note) diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index b5ce56dbc..f49594828 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -17,16 +17,20 @@ - if @account.local? %tr %th= t('admin.accounts.email') - %td= @account.user_email + %td + = @account.user_email + + - if @account.user_confirmed? + = fa_icon('check') %tr %th= t('admin.accounts.login_status') %td - if @account.user&.disabled? = t('admin.accounts.disabled') - = table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post + = table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post if can?(:enable, @account.user) - else = t('admin.accounts.enabled') - = table_link_to 'lock', t('admin.accounts.disable'), disable_admin_account_path(@account.id), method: :post + = table_link_to 'lock', t('admin.accounts.disable'), disable_admin_account_path(@account.id), method: :post if can?(:disable, @account.user) %tr %th= t('admin.accounts.most_recent_ip') %td= @account.user_current_sign_in_ip @@ -71,28 +75,28 @@ %div{ style: 'overflow: hidden' } %div{ style: 'float: right' } - if @account.local? - = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' + = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user) - if @account.user&.otp_required_for_login? - = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' + = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user) - unless @account.memorial? - = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' + = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:memorialize, @account) - else - = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' + = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account) %div{ style: 'float: left' } - if @account.silenced? - = link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button' + = link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button' if can?(:unsilence, @account) - else - = link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button' + = link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button' if can?(:silence, @account) - if @account.local? - unless @account.user_confirmed? - = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' + = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user) - if @account.suspended? - = link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button' + = link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button' if can?(:unsuspend, @account) - else - = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' + = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:suspend, @account) - unless @account.local? %hr @@ -118,9 +122,9 @@ %div{ style: 'overflow: hidden' } %div{ style: 'float: right' } - = link_to @account.subscribed? ? t('admin.accounts.resubscribe') : t('admin.accounts.subscribe'), subscribe_admin_account_path(@account.id), method: :post, class: 'button' + = link_to @account.subscribed? ? t('admin.accounts.resubscribe') : t('admin.accounts.subscribe'), subscribe_admin_account_path(@account.id), method: :post, class: 'button' if can?(:subscribe, @account) - if @account.subscribed? - = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' + = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' if can?(:unsubscribe, @account) %hr %h3 ActivityPub @@ -141,6 +145,20 @@ %th= t('admin.accounts.followers_url') %td= link_to @account.followers_url, @account.followers_url +- else + %hr + + .table-wrapper + %table.table + %tbody + %tr + %th= t('admin.accounts.role') + %td + = t("admin.accounts.roles.#{@account.user&.role}") + %td< + = table_link_to 'angle-double-up', t('admin.accounts.promote'), promote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:promote, @account.user) + = table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user) + %hr %h3= t('admin.accounts.moderation_notes') diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index b35e5c09a..08a96f727 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -46,6 +46,7 @@ ignore_missing: - 'terms.body_html' - 'application_mailer.salutation' - 'errors.500' + ignore_unused: - 'activemodel.errors.*' - 'activerecord.attributes.*' @@ -58,3 +59,4 @@ ignore_unused: - 'errors.messages.*' - 'activerecord.errors.models.doorkeeper/*' - 'errors.429' + - 'admin.accounts.roles.*' diff --git a/config/locales/en.yml b/config/locales/en.yml index be0431ed3..e94165317 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -62,6 +62,7 @@ en: by_domain: Domain confirm: Confirm confirmed: Confirmed + demote: Demote disable: Disable disable_two_factor_authentication: Disable 2FA disabled: Disabled @@ -101,6 +102,7 @@ en: outbox_url: Outbox URL perform_full_suspension: Perform full suspension profile_url: Profile URL + promote: Promote protocol: Protocol public: Public push_subscription_expires: PuSH subscription expires @@ -108,6 +110,11 @@ en: reset: Reset reset_password: Reset password resubscribe: Resubscribe + role: Permissions + roles: + admin: Administrator + moderator: Moderator + user: User salmon_url: Salmon URL search: Search shared_inbox_url: Shared Inbox URL diff --git a/config/navigation.rb b/config/navigation.rb index 50bfbd480..5b4800f07 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -20,16 +20,16 @@ SimpleNavigation::Configuration.run do |navigation| development.item :your_apps, safe_join([fa_icon('list fw'), t('settings.your_apps')]), settings_applications_url, highlights_on: %r{/settings/applications} end - primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_reports_url, if: proc { current_user.admin? } do |admin| + primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_reports_url, if: proc { current_user.staff? } do |admin| admin.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports} admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts} - admin.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url, highlights_on: %r{/admin/instances} - admin.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url - admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_blocks.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks} - admin.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks} - admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' } - admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' } - admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url + admin.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url, highlights_on: %r{/admin/instances}, if: -> { current_user.admin? } + admin.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? } + admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_blocks.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}, if: -> { current_user.admin? } + admin.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? } + admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? } + admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? } + admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? } admin.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis} end diff --git a/config/routes.rb b/config/routes.rb index e6d6b52f7..9301a4e50 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -137,6 +137,13 @@ Rails.application.routes.draw do resource :suspension, only: [:create, :destroy] resource :confirmation, only: [:create] resources :statuses, only: [:index, :create, :update, :destroy] + + resource :role do + member do + post :promote + post :demote + end + end end resources :users, only: [] do diff --git a/db/migrate/20171109012327_add_moderator_to_accounts.rb b/db/migrate/20171109012327_add_moderator_to_accounts.rb new file mode 100644 index 000000000..ddd87583a --- /dev/null +++ b/db/migrate/20171109012327_add_moderator_to_accounts.rb @@ -0,0 +1,15 @@ +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddModeratorToAccounts < ActiveRecord::Migration[5.1] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def up + safety_assured { add_column_with_default :users, :moderator, :bool, default: false } + end + + def down + remove_column :users, :moderator + end +end diff --git a/db/schema.rb b/db/schema.rb index 935fd79c5..f16b24fd6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171107143624) do +ActiveRecord::Schema.define(version: 20171109012327) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -437,6 +437,7 @@ ActiveRecord::Schema.define(version: 20171107143624) do t.string "filtered_languages", default: [], null: false, array: true t.bigint "account_id", null: false t.boolean "disabled", default: false, null: false + t.boolean "moderator", default: false, null: false t.index ["account_id"], name: "index_users_on_account_id" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 4d519bf90..995cf0d6f 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -10,14 +10,41 @@ namespace :mastodon do desc 'Turn a user into an admin, identified by the USERNAME environment variable' task make_admin: :environment do include RoutingHelper + account_username = ENV.fetch('USERNAME') - user = User.joins(:account).where(accounts: { username: account_username }) + user = User.joins(:account).where(accounts: { username: account_username }) if user.present? user.update(admin: true) puts "Congrats! #{account_username} is now an admin. \\o/\nNavigate to #{edit_admin_settings_url} to get started" else - puts "User could not be found; please make sure an Account with the `#{account_username}` username exists." + puts "User could not be found; please make sure an account with the `#{account_username}` username exists." + end + end + + desc 'Turn a user into a moderator, identified by the USERNAME environment variable' + task make_mod: :environment do + account_username = ENV.fetch('USERNAME') + user = User.joins(:account).where(accounts: { username: account_username }) + + if user.present? + user.update(moderator: true) + puts "Congrats! #{account_username} is now a moderator \\o/" + else + puts "User could not be found; please make sure an account with the `#{account_username}` username exists." + end + end + + desc 'Remove admin and moderator privileges from user identified by the USERNAME environment variable' + task revoke_staff: :environment do + account_username = ENV.fetch('USERNAME') + user = User.joins(:account).where(accounts: { username: account_username }) + + if user.present? + user.update(moderator: false, admin: false) + puts "#{account_username} is no longer admin or moderator." + else + puts "User could not be found; please make sure an account with the `#{account_username}` username exists." end end -- cgit From cf7e8409909e1ea02dff9056133449d6cbd34d72 Mon Sep 17 00:00:00 2001 From: Daniel Hunsaker Date: Sun, 12 Nov 2017 08:18:50 -0700 Subject: Update model annotations to use BIGINT for IDs (#5461) All the migrations have been updated to use BIGINTs for ID fields in the DB, but ActiveRecord needs to be told to treat those values as BIGINT as well. This PR does that. --- app/models/account.rb | 2 +- app/models/account_domain_block.rb | 4 ++-- app/models/account_moderation_note.rb | 6 +++--- app/models/block.rb | 6 +++--- app/models/conversation.rb | 2 +- app/models/conversation_mute.rb | 6 +++--- app/models/custom_emoji.rb | 2 +- app/models/domain_block.rb | 2 +- app/models/email_domain_block.rb | 2 +- app/models/favourite.rb | 6 +++--- app/models/follow.rb | 6 +++--- app/models/follow_request.rb | 6 +++--- app/models/import.rb | 4 ++-- app/models/media_attachment.rb | 6 +++--- app/models/mention.rb | 6 +++--- app/models/mute.rb | 6 +++--- app/models/notification.rb | 8 ++++---- app/models/preview_card.rb | 2 +- app/models/report.rb | 8 ++++---- app/models/session_activation.rb | 14 +++++++------- app/models/setting.rb | 4 ++-- app/models/site_upload.rb | 2 +- app/models/status.rb | 14 +++++++------- app/models/status_pin.rb | 6 +++--- app/models/stream_entry.rb | 6 +++--- app/models/subscription.rb | 4 ++-- app/models/tag.rb | 2 +- app/models/user.rb | 4 ++-- app/models/web/push_subscription.rb | 2 +- app/models/web/setting.rb | 4 ++-- 30 files changed, 76 insertions(+), 76 deletions(-) (limited to 'app/models') diff --git a/app/models/account.rb b/app/models/account.rb index 7e4d29f96..6f6010f7a 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -3,7 +3,7 @@ # # Table name: accounts # -# id :integer not null, primary key +# id :bigint not null, primary key # username :string default(""), not null # domain :string # secret :string default(""), not null diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb index fb695e473..9c98ec2a6 100644 --- a/app/models/account_domain_block.rb +++ b/app/models/account_domain_block.rb @@ -6,8 +6,8 @@ # domain :string # created_at :datetime not null # updated_at :datetime not null -# account_id :integer -# id :integer not null, primary key +# account_id :bigint +# id :bigint not null, primary key # class AccountDomainBlock < ApplicationRecord diff --git a/app/models/account_moderation_note.rb b/app/models/account_moderation_note.rb index 3ac9b1ac1..06f464850 100644 --- a/app/models/account_moderation_note.rb +++ b/app/models/account_moderation_note.rb @@ -3,10 +3,10 @@ # # Table name: account_moderation_notes # -# id :integer not null, primary key +# id :bigint not null, primary key # content :text not null -# account_id :integer not null -# target_account_id :integer not null +# account_id :bigint not null +# target_account_id :bigint not null # created_at :datetime not null # updated_at :datetime not null # diff --git a/app/models/block.rb b/app/models/block.rb index a913782ed..5778f7e90 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -5,9 +5,9 @@ # # created_at :datetime not null # updated_at :datetime not null -# account_id :integer not null -# id :integer not null, primary key -# target_account_id :integer not null +# account_id :bigint not null +# id :bigint not null, primary key +# target_account_id :bigint not null # class Block < ApplicationRecord diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 08c1ce945..e08532522 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -3,7 +3,7 @@ # # Table name: conversations # -# id :integer not null, primary key +# id :bigint not null, primary key # uri :string # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/conversation_mute.rb b/app/models/conversation_mute.rb index 8d2399adf..316865bd2 100644 --- a/app/models/conversation_mute.rb +++ b/app/models/conversation_mute.rb @@ -3,9 +3,9 @@ # # Table name: conversation_mutes # -# conversation_id :integer not null -# account_id :integer not null -# id :integer not null, primary key +# conversation_id :bigint not null +# account_id :bigint not null +# id :bigint not null, primary key # class ConversationMute < ApplicationRecord diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index a77b53c98..5723ebd5d 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -3,7 +3,7 @@ # # Table name: custom_emojis # -# id :integer not null, primary key +# id :bigint not null, primary key # shortcode :string default(""), not null # domain :string # image_file_name :string diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 1268290bc..557d0a19c 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -8,7 +8,7 @@ # updated_at :datetime not null # severity :integer default("silence") # reject_media :boolean default(FALSE), not null -# id :integer not null, primary key +# id :bigint not null, primary key # class DomainBlock < ApplicationRecord diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb index 839038bea..51410605b 100644 --- a/app/models/email_domain_block.rb +++ b/app/models/email_domain_block.rb @@ -3,7 +3,7 @@ # # Table name: email_domain_blocks # -# id :integer not null, primary key +# id :bigint not null, primary key # domain :string not null # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/favourite.rb b/app/models/favourite.rb index d28d5c05b..f611aa6a9 100644 --- a/app/models/favourite.rb +++ b/app/models/favourite.rb @@ -5,9 +5,9 @@ # # created_at :datetime not null # updated_at :datetime not null -# account_id :integer not null -# id :integer not null, primary key -# status_id :integer not null +# account_id :bigint not null +# id :bigint not null, primary key +# status_id :bigint not null # class Favourite < ApplicationRecord diff --git a/app/models/follow.rb b/app/models/follow.rb index 667720a88..3d5447fb1 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -5,9 +5,9 @@ # # created_at :datetime not null # updated_at :datetime not null -# account_id :integer not null -# id :integer not null, primary key -# target_account_id :integer not null +# account_id :bigint not null +# id :bigint not null, primary key +# target_account_id :bigint not null # class Follow < ApplicationRecord diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 458bcd28a..ce27fc921 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -5,9 +5,9 @@ # # created_at :datetime not null # updated_at :datetime not null -# account_id :integer not null -# id :integer not null, primary key -# target_account_id :integer not null +# account_id :bigint not null +# id :bigint not null, primary key +# target_account_id :bigint not null # class FollowRequest < ApplicationRecord diff --git a/app/models/import.rb b/app/models/import.rb index 8ae7e3a46..6f1278556 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -11,8 +11,8 @@ # data_content_type :string # data_file_size :integer # data_updated_at :datetime -# account_id :integer not null -# id :integer not null, primary key +# account_id :bigint not null +# id :bigint not null, primary key # class Import < ApplicationRecord diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 60380198b..f05418925 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -3,14 +3,14 @@ # # Table name: media_attachments # -# id :integer not null, primary key -# status_id :integer +# id :bigint not null, primary key +# status_id :bigint # file_file_name :string # file_content_type :string # file_file_size :integer # file_updated_at :datetime # remote_url :string default(""), not null -# account_id :integer +# account_id :bigint # created_at :datetime not null # updated_at :datetime not null # shortcode :string diff --git a/app/models/mention.rb b/app/models/mention.rb index 3700c781c..fc089d365 100644 --- a/app/models/mention.rb +++ b/app/models/mention.rb @@ -3,11 +3,11 @@ # # Table name: mentions # -# status_id :integer +# status_id :bigint # created_at :datetime not null # updated_at :datetime not null -# account_id :integer -# id :integer not null, primary key +# account_id :bigint +# id :bigint not null, primary key # class Mention < ApplicationRecord diff --git a/app/models/mute.rb b/app/models/mute.rb index 6e64848c7..4174a3523 100644 --- a/app/models/mute.rb +++ b/app/models/mute.rb @@ -5,9 +5,9 @@ # # created_at :datetime not null # updated_at :datetime not null -# account_id :integer not null -# id :integer not null, primary key -# target_account_id :integer not null +# account_id :bigint not null +# id :bigint not null, primary key +# target_account_id :bigint not null # class Mute < ApplicationRecord diff --git a/app/models/notification.rb b/app/models/notification.rb index 0a5d987cf..c88af9021 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -3,13 +3,13 @@ # # Table name: notifications # -# id :integer not null, primary key -# account_id :integer -# activity_id :integer +# id :bigint not null, primary key +# account_id :bigint +# activity_id :bigint # activity_type :string # created_at :datetime not null # updated_at :datetime not null -# from_account_id :integer +# from_account_id :bigint # class Notification < ApplicationRecord diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index e2bf65d94..63c04b410 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -3,7 +3,7 @@ # # Table name: preview_cards # -# id :integer not null, primary key +# id :bigint not null, primary key # url :string default(""), not null # title :string default(""), not null # description :string default(""), not null diff --git a/app/models/report.rb b/app/models/report.rb index bffb42b48..99c90b7dd 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -8,10 +8,10 @@ # action_taken :boolean default(FALSE), not null # created_at :datetime not null # updated_at :datetime not null -# account_id :integer not null -# action_taken_by_account_id :integer -# id :integer not null, primary key -# target_account_id :integer not null +# account_id :bigint not null +# action_taken_by_account_id :bigint +# id :bigint not null, primary key +# target_account_id :bigint not null # class Report < ApplicationRecord diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb index c1645223b..59565f877 100644 --- a/app/models/session_activation.rb +++ b/app/models/session_activation.rb @@ -3,25 +3,25 @@ # # Table name: session_activations # -# id :integer not null, primary key -# user_id :integer not null +# id :bigint not null, primary key +# user_id :bigint not null # session_id :string not null # created_at :datetime not null # updated_at :datetime not null # user_agent :string default(""), not null # ip :inet -# access_token_id :integer -# web_push_subscription_id :integer +# access_token_id :bigint +# web_push_subscription_id :bigint # -# id :integer not null, primary key -# user_id :integer not null +# id :bigint not null, primary key +# user_id :bigint not null # session_id :string not null # created_at :datetime not null # updated_at :datetime not null # user_agent :string default(""), not null # ip :inet -# access_token_id :integer +# access_token_id :bigint # class SessionActivation < ApplicationRecord diff --git a/app/models/setting.rb b/app/models/setting.rb index a14f156a1..be68d3123 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -8,8 +8,8 @@ # thing_type :string # created_at :datetime # updated_at :datetime -# id :integer not null, primary key -# thing_id :integer +# id :bigint not null, primary key +# thing_id :bigint # class Setting < RailsSettings::Base diff --git a/app/models/site_upload.rb b/app/models/site_upload.rb index 8ffdc8313..ba2ca777b 100644 --- a/app/models/site_upload.rb +++ b/app/models/site_upload.rb @@ -3,7 +3,7 @@ # # Table name: site_uploads # -# id :integer not null, primary key +# id :bigint not null, primary key # var :string default(""), not null # file_file_name :string # file_content_type :string diff --git a/app/models/status.rb b/app/models/status.rb index 5a7245613..b4f314311 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -3,25 +3,25 @@ # # Table name: statuses # -# id :integer not null, primary key +# id :bigint not null, primary key # uri :string -# account_id :integer not null +# account_id :bigint not null # text :text default(""), not null # created_at :datetime not null # updated_at :datetime not null -# in_reply_to_id :integer -# reblog_of_id :integer +# in_reply_to_id :bigint +# reblog_of_id :bigint # url :string # sensitive :boolean default(FALSE), not null # visibility :integer default("public"), not null -# in_reply_to_account_id :integer -# application_id :integer +# in_reply_to_account_id :bigint +# application_id :bigint # spoiler_text :text default(""), not null # reply :boolean default(FALSE), not null # favourites_count :integer default(0), not null # reblogs_count :integer default(0), not null # language :string -# conversation_id :integer +# conversation_id :bigint # local :boolean # diff --git a/app/models/status_pin.rb b/app/models/status_pin.rb index a72c19750..5795d07bf 100644 --- a/app/models/status_pin.rb +++ b/app/models/status_pin.rb @@ -3,9 +3,9 @@ # # Table name: status_pins # -# id :integer not null, primary key -# account_id :integer not null -# status_id :integer not null +# id :bigint not null, primary key +# account_id :bigint not null +# status_id :bigint not null # created_at :datetime not null # updated_at :datetime not null # diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb index b51fe9ad7..50b900c3c 100644 --- a/app/models/stream_entry.rb +++ b/app/models/stream_entry.rb @@ -3,13 +3,13 @@ # # Table name: stream_entries # -# activity_id :integer +# activity_id :bigint # activity_type :string # created_at :datetime not null # updated_at :datetime not null # hidden :boolean default(FALSE), not null -# account_id :integer -# id :integer not null, primary key +# account_id :bigint +# id :bigint not null, primary key # class StreamEntry < ApplicationRecord diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 39860196b..bc50c5317 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -11,8 +11,8 @@ # updated_at :datetime not null # last_successful_delivery_at :datetime # domain :string -# account_id :integer not null -# id :integer not null, primary key +# account_id :bigint not null +# id :bigint not null, primary key # class Subscription < ApplicationRecord diff --git a/app/models/tag.rb b/app/models/tag.rb index 0fa08e157..6ebaf1145 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -3,7 +3,7 @@ # # Table name: tags # -# id :integer not null, primary key +# id :bigint not null, primary key # name :string default(""), not null # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/user.rb b/app/models/user.rb index 9022e6ea8..ebe768c52 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,7 +3,7 @@ # # Table name: users # -# id :integer not null, primary key +# id :bigint not null, primary key # email :string default(""), not null # created_at :datetime not null # updated_at :datetime not null @@ -30,7 +30,7 @@ # last_emailed_at :datetime # otp_backup_codes :string is an Array # filtered_languages :string default([]), not null, is an Array -# account_id :integer not null +# account_id :bigint not null # disabled :boolean default(FALSE), not null # moderator :boolean default(FALSE), not null # diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index cb15dfa37..da67e7665 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -3,7 +3,7 @@ # # Table name: web_push_subscriptions # -# id :integer not null, primary key +# id :bigint not null, primary key # endpoint :string not null # key_p256dh :string not null # key_auth :string not null diff --git a/app/models/web/setting.rb b/app/models/web/setting.rb index 1b0bfb2b7..6d08c4d35 100644 --- a/app/models/web/setting.rb +++ b/app/models/web/setting.rb @@ -6,8 +6,8 @@ # data :json # created_at :datetime not null # updated_at :datetime not null -# id :integer not null, primary key -# user_id :integer +# id :bigint not null, primary key +# user_id :bigint # class Web::Setting < ApplicationRecord -- cgit From 20150659e6f084d6c6fb4080d08c4104b8ac0570 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Wed, 15 Nov 2017 04:37:17 +0900 Subject: Add uniqueness to block email domains (#5692) --- app/models/email_domain_block.rb | 23 ++++++++++++++++++++-- ...0328_add_index_domain_to_email_domain_blocks.rb | 8 ++++++++ db/schema.rb | 5 +++-- spec/models/email_domain_block_spec.rb | 5 +++-- 4 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20171114080328_add_index_domain_to_email_domain_blocks.rb (limited to 'app/models') diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb index 51410605b..2c348197c 100644 --- a/app/models/email_domain_block.rb +++ b/app/models/email_domain_block.rb @@ -4,14 +4,33 @@ # Table name: email_domain_blocks # # id :bigint not null, primary key -# domain :string not null +# domain :string default(""), not null # created_at :datetime not null # updated_at :datetime not null # class EmailDomainBlock < ApplicationRecord + before_validation :normalize_domain + + validates :domain, presence: true, uniqueness: true + def self.block?(email) - domain = email.gsub(/.+@([^.]+)/, '\1') + _, domain = email.split('@', 2) + + return true if domain.nil? + + begin + domain = TagManager.instance.normalize_domain(domain) + rescue Addressable::URI::InvalidURIError + return true + end + where(domain: domain).exists? end + + private + + def normalize_domain + self.domain = TagManager.instance.normalize_domain(domain) + end end diff --git a/db/migrate/20171114080328_add_index_domain_to_email_domain_blocks.rb b/db/migrate/20171114080328_add_index_domain_to_email_domain_blocks.rb new file mode 100644 index 000000000..84a341510 --- /dev/null +++ b/db/migrate/20171114080328_add_index_domain_to_email_domain_blocks.rb @@ -0,0 +1,8 @@ +class AddIndexDomainToEmailDomainBlocks < ActiveRecord::Migration[5.1] + disable_ddl_transaction! + + def change + add_index :email_domain_blocks, :domain, algorithm: :concurrently, unique: true + change_column_default :email_domain_blocks, :domain, from: nil, to: '' + end +end diff --git a/db/schema.rb b/db/schema.rb index f16b24fd6..bf319ce55 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171109012327) do +ActiveRecord::Schema.define(version: 20171114080328) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -126,9 +126,10 @@ ActiveRecord::Schema.define(version: 20171109012327) do end create_table "email_domain_blocks", force: :cascade do |t| - t.string "domain", null: false + t.string "domain", default: "", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["domain"], name: "index_email_domain_blocks_on_domain", unique: true end create_table "favourites", force: :cascade do |t| diff --git a/spec/models/email_domain_block_spec.rb b/spec/models/email_domain_block_spec.rb index 5f5d189d9..efd2853a9 100644 --- a/spec/models/email_domain_block_spec.rb +++ b/spec/models/email_domain_block_spec.rb @@ -13,9 +13,10 @@ RSpec.describe EmailDomainBlock, type: :model do Fabricate(:email_domain_block, domain: 'example.com') expect(EmailDomainBlock.block?('nyarn@example.com')).to eq true end + it 'returns true if the domain is not registed' do - Fabricate(:email_domain_block, domain: 'domain') - expect(EmailDomainBlock.block?('example')).to eq false + Fabricate(:email_domain_block, domain: 'example.com') + expect(EmailDomainBlock.block?('nyarn@example.net')).to eq false end end end -- cgit From 031a5a8f922d422ff087ad2fca82e3557a1b29d9 Mon Sep 17 00:00:00 2001 From: Surinna Curtis Date: Tue, 14 Nov 2017 20:56:41 -0600 Subject: Optional notification muting (#5087) * Add a hide_notifications column to mutes * Add muting_notifications? and a notifications argument to mute! * block notifications in notify_service from hard muted accounts * Add specs for how mute! interacts with muting_notifications? * specs testing that hide_notifications in mutes actually hides notifications * Add support for muting notifications in MuteService * API support for muting notifications (and specs) * Less gross passing of notifications flag * Break out a separate mute modal with a hide-notifications checkbox. * Convert profile header mute to use mute modal * Satisfy eslint. * specs for MuteService notifications params * add trailing newlines to files for Pork :) * Put the label for the hide notifications checkbox in a label element. * Add a /api/v1/mutes/details route that just returns the array of mutes. * Define a serializer for /api/v1/mutes/details * Add more specs for the /api/v1/mutes/details endpoint * Expose whether a mute hides notifications in the api/v1/relationships endpoint * Show whether muted users' notifications are muted in account lists * Allow modifying the hide_notifications of a mute with the /api/v1/accounts/:id/mute endpoint * make the hide/unhide notifications buttons work * satisfy eslint * In probably dead code, replace a dispatch of muteAccount that was skipping the modal with launching the mute modal. * fix a missing import * add an explanatory comment to AccountInteractions * Refactor handling of default params for muting to make code cleaner * minor code style fixes oops * Fixed a typo that was breaking the account mute API endpoint * Apply white-space: nowrap to account relationships icons * Fix code style issues * Remove superfluous blank line * Rename /api/v1/mutes/details -> /api/v2/mutes * Don't serialize "account" in MuteSerializer Doing so is somewhat unnecessary since it's always the current user's account. * Fix wrong variable name in api/v2/mutes * Use Toggle in place of checkbox in the mute modal. * Make the Toggle in the mute modal look better * Code style changes in specs and removed an extra space * Code review suggestions from akihikodaki Also fixed a syntax error in tests for AccountInteractions. * Make AddHideNotificationsToMute Concurrent It's not clear how much this will benefit instances in practice, as the number of mutes tends to be pretty small, but this should prevent any blocking migrations nonetheless. * Fix up migration things * Remove /api/v2/mutes --- app/controllers/api/v1/accounts_controller.rb | 2 +- app/javascript/mastodon/actions/accounts.js | 4 +- app/javascript/mastodon/actions/mutes.js | 21 +++++ app/javascript/mastodon/components/account.js | 23 ++++- .../mastodon/containers/account_container.js | 7 +- .../mastodon/containers/status_container.js | 13 +-- .../containers/header_container.js | 9 +- .../mastodon/features/ui/components/modal_root.js | 2 + .../mastodon/features/ui/components/mute_modal.js | 105 +++++++++++++++++++++ .../mastodon/features/ui/util/async-components.js | 4 + app/javascript/mastodon/reducers/index.js | 2 + app/javascript/mastodon/reducers/mutes.js | 29 ++++++ app/javascript/styles/mastodon/components.scss | 20 +++- app/models/concerns/account_interactions.rb | 19 +++- app/models/mute.rb | 11 ++- app/services/mute_service.rb | 5 +- app/services/notify_service.rb | 2 +- ...0170716191202_add_hide_notifications_to_mute.rb | 15 +++ db/schema.rb | 1 + .../controllers/api/v1/accounts_controller_spec.rb | 29 ++++++ spec/controllers/api/v1/mutes_controller_spec.rb | 2 +- spec/models/concerns/account_interactions_spec.rb | 38 ++++++++ spec/services/mute_service_spec.rb | 32 +++++++ spec/services/notify_service_spec.rb | 10 ++ 24 files changed, 368 insertions(+), 37 deletions(-) create mode 100644 app/javascript/mastodon/features/ui/components/mute_modal.js create mode 100644 app/javascript/mastodon/reducers/mutes.js create mode 100644 db/migrate/20170716191202_add_hide_notifications_to_mute.rb create mode 100644 spec/models/concerns/account_interactions_spec.rb (limited to 'app/models') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index b3fc4e561..4676f60de 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -26,7 +26,7 @@ class Api::V1::AccountsController < Api::BaseController end def mute - MuteService.new.call(current_user.account, @account) + MuteService.new.call(current_user.account, @account, notifications: params[:notifications]) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index 73d6baace..fbaebf786 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -241,11 +241,11 @@ export function unblockAccountFail(error) { }; -export function muteAccount(id) { +export function muteAccount(id, notifications) { return (dispatch, getState) => { dispatch(muteAccountRequest(id)); - api(getState).post(`/api/v1/accounts/${id}/mute`).then(response => { + api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); }).catch(error => { diff --git a/app/javascript/mastodon/actions/mutes.js b/app/javascript/mastodon/actions/mutes.js index febda7219..3474250fe 100644 --- a/app/javascript/mastodon/actions/mutes.js +++ b/app/javascript/mastodon/actions/mutes.js @@ -1,5 +1,6 @@ import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; +import { openModal } from '../../mastodon/actions/modal'; export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; @@ -9,6 +10,9 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST'; export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS'; export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; +export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL'; +export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS'; + export function fetchMutes() { return (dispatch, getState) => { dispatch(fetchMutesRequest()); @@ -80,3 +84,20 @@ export function expandMutesFail(error) { error, }; }; + +export function initMuteModal(account) { + return dispatch => { + dispatch({ + type: MUTES_INIT_MODAL, + account, + }); + + dispatch(openModal('MUTE')); + }; +} + +export function toggleHideNotifications() { + return dispatch => { + dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); + }; +} \ No newline at end of file diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index 0e3007ce8..724b10980 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -15,6 +15,8 @@ const messages = defineMessages({ requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, + mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' }, + unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' }, }); @injectIntl @@ -41,6 +43,14 @@ export default class Account extends ImmutablePureComponent { this.props.onMute(this.props.account); } + handleMuteNotifications = () => { + this.props.onMuteNotifications(this.props.account, true); + } + + handleUnmuteNotifications = () => { + this.props.onMuteNotifications(this.props.account, false); + } + render () { const { account, intl, hidden } = this.props; @@ -70,7 +80,18 @@ export default class Account extends ImmutablePureComponent { } else if (blocking) { buttons = ; } else if (muting) { - buttons = ; + let hidingNotificationsButton; + if (muting.get('notifications')) { + hidingNotificationsButton = ; + } else { + hidingNotificationsButton = ; + } + buttons = ( +
+ + {hidingNotificationsButton} +
+ ); } else { buttons = ; } diff --git a/app/javascript/mastodon/containers/account_container.js b/app/javascript/mastodon/containers/account_container.js index 344f6749d..5a5136dd1 100644 --- a/app/javascript/mastodon/containers/account_container.js +++ b/app/javascript/mastodon/containers/account_container.js @@ -12,6 +12,7 @@ import { unmuteAccount, } from '../actions/accounts'; import { openModal } from '../actions/modal'; +import { initMuteModal } from '../actions/mutes'; import { unfollowModal } from '../initial_state'; const messages = defineMessages({ @@ -58,10 +59,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ if (account.getIn(['relationship', 'muting'])) { dispatch(unmuteAccount(account.get('id'))); } else { - dispatch(muteAccount(account.get('id'))); + dispatch(initMuteModal(account)); } }, + + onMuteNotifications (account, notifications) { + dispatch(muteAccount(account.get('id'), notifications)); + }, }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account)); diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index 311ccae5b..b22540204 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -14,11 +14,9 @@ import { pin, unpin, } from '../actions/interactions'; -import { - blockAccount, - muteAccount, -} from '../actions/accounts'; +import { blockAccount } from '../actions/accounts'; import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses'; +import { initMuteModal } from '../actions/mutes'; import { initReport } from '../actions/reports'; import { openModal } from '../actions/modal'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; @@ -28,7 +26,6 @@ const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, - muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' }, }); const makeMapStateToProps = () => { @@ -120,11 +117,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onMute (account) { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.muteConfirm), - onConfirm: () => dispatch(muteAccount(account.get('id'))), - })); + dispatch(initMuteModal(account)); }, onMuteConversation (status) { diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index 01e18928e..8e50ec405 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -7,10 +7,10 @@ import { unfollowAccount, blockAccount, unblockAccount, - muteAccount, unmuteAccount, } from '../../../actions/accounts'; import { mentionCompose } from '../../../actions/compose'; +import { initMuteModal } from '../../../actions/mutes'; import { initReport } from '../../../actions/reports'; import { openModal } from '../../../actions/modal'; import { blockDomain, unblockDomain } from '../../../actions/domain_blocks'; @@ -20,7 +20,6 @@ import { unfollowModal } from '../../../initial_state'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, - muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, }); @@ -76,11 +75,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ if (account.getIn(['relationship', 'muting'])) { dispatch(unmuteAccount(account.get('id'))); } else { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.muteConfirm), - onConfirm: () => dispatch(muteAccount(account.get('id'))), - })); + dispatch(initMuteModal(account)); } }, diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index f420f0abf..79d86370e 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -10,6 +10,7 @@ import BoostModal from './boost_modal'; import ConfirmationModal from './confirmation_modal'; import { OnboardingModal, + MuteModal, ReportModal, EmbedModal, } from '../../../features/ui/util/async-components'; @@ -20,6 +21,7 @@ const MODAL_COMPONENTS = { 'VIDEO': () => Promise.resolve({ default: VideoModal }), 'BOOST': () => Promise.resolve({ default: BoostModal }), 'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), + 'MUTE': MuteModal, 'REPORT': ReportModal, 'ACTIONS': () => Promise.resolve({ default: ActionsModal }), 'EMBED': EmbedModal, diff --git a/app/javascript/mastodon/features/ui/components/mute_modal.js b/app/javascript/mastodon/features/ui/components/mute_modal.js new file mode 100644 index 000000000..73e48cf09 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/mute_modal.js @@ -0,0 +1,105 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import Toggle from 'react-toggle'; +import Button from '../../../components/button'; +import { closeModal } from '../../../actions/modal'; +import { muteAccount } from '../../../actions/accounts'; +import { toggleHideNotifications } from '../../../actions/mutes'; + + +const mapStateToProps = state => { + return { + isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), + account: state.getIn(['mutes', 'new', 'account']), + notifications: state.getIn(['mutes', 'new', 'notifications']), + }; +}; + +const mapDispatchToProps = dispatch => { + return { + onConfirm(account, notifications) { + dispatch(muteAccount(account.get('id'), notifications)); + }, + + onClose() { + dispatch(closeModal()); + }, + + onToggleNotifications() { + dispatch(toggleHideNotifications()); + }, + }; +}; + +@connect(mapStateToProps, mapDispatchToProps) +@injectIntl +export default class MuteModal extends React.PureComponent { + + static propTypes = { + isSubmitting: PropTypes.bool.isRequired, + account: PropTypes.object.isRequired, + notifications: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + onToggleNotifications: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + componentDidMount() { + this.button.focus(); + } + + handleClick = () => { + this.props.onClose(); + this.props.onConfirm(this.props.account, this.props.notifications); + } + + handleCancel = () => { + this.props.onClose(); + } + + setRef = (c) => { + this.button = c; + } + + toggleNotifications = () => { + this.props.onToggleNotifications(); + } + + render () { + const { account, notifications } = this.props; + + return ( +
+
+

+ @{account.get('acct')} }} + /> +

+
+ +
+
+ +
+ + +
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 8f7b91d21..39663d5ca 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -86,6 +86,10 @@ export function OnboardingModal () { return import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal'); } +export function MuteModal () { + return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal'); +} + export function ReportModal () { return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal'); } diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index e65144871..17c870351 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -13,6 +13,7 @@ import settings from './settings'; import push_notifications from './push_notifications'; import status_lists from './status_lists'; import cards from './cards'; +import mutes from './mutes'; import reports from './reports'; import contexts from './contexts'; import compose from './compose'; @@ -37,6 +38,7 @@ const reducers = { settings, push_notifications, cards, + mutes, reports, contexts, compose, diff --git a/app/javascript/mastodon/reducers/mutes.js b/app/javascript/mastodon/reducers/mutes.js new file mode 100644 index 000000000..a96232dbd --- /dev/null +++ b/app/javascript/mastodon/reducers/mutes.js @@ -0,0 +1,29 @@ +import Immutable from 'immutable'; + +import { + MUTES_INIT_MODAL, + MUTES_TOGGLE_HIDE_NOTIFICATIONS, +} from '../actions/mutes'; + +const initialState = Immutable.Map({ + new: Immutable.Map({ + isSubmitting: false, + account: null, + notifications: true, + }), +}); + +export default function mutes(state = initialState, action) { + switch (action.type) { + case MUTES_INIT_MODAL: + return state.withMutations((state) => { + state.setIn(['new', 'isSubmitting'], false); + state.setIn(['new', 'account'], action.account); + state.setIn(['new', 'notifications'], true); + }); + case MUTES_TOGGLE_HIDE_NOTIFICATIONS: + return state.updateIn(['new', 'notifications'], (old) => !old); + default: + return state; + } +} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index e4504f543..0ded6f159 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -906,6 +906,7 @@ .account__relationship { height: 18px; padding: 10px; + white-space: nowrap; } .account__header { @@ -3515,7 +3516,8 @@ button.icon-button.active i.fa-retweet { .boost-modal, .confirmation-modal, .report-modal, -.actions-modal { +.actions-modal, +.mute-modal { background: lighten($ui-secondary-color, 8%); color: $ui-base-color; border-radius: 8px; @@ -3565,6 +3567,7 @@ button.icon-button.active i.fa-retweet { .boost-modal__action-bar, .confirmation-modal__action-bar, +.mute-modal__action-bar, .report-modal__action-bar { display: flex; justify-content: space-between; @@ -3601,6 +3604,14 @@ button.icon-button.active i.fa-retweet { } } +.mute-modal { + line-height: 24px; +} + +.mute-modal .react-toggle { + vertical-align: middle; +} + .report-modal__statuses, .report-modal__comment { padding: 10px; @@ -3673,8 +3684,10 @@ button.icon-button.active i.fa-retweet { } } -.confirmation-modal__action-bar { - .confirmation-modal__cancel-button { +.confirmation-modal__action-bar, +.mute-modal__action-bar { + .confirmation-modal__cancel-button, + .mute-modal__cancel-button { background-color: transparent; color: darken($ui-secondary-color, 34%); font-size: 14px; @@ -3689,6 +3702,7 @@ button.icon-button.active i.fa-retweet { } .confirmation-modal__container, +.mute-modal__container, .report-modal__target { padding: 30px; font-size: 16px; diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index b26520f5b..55ad812b2 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -17,7 +17,11 @@ module AccountInteractions end def muting_map(target_account_ids, account_id) - follow_mapping(Mute.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) + Mute.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |mute, mapping| + mapping[mute.target_account_id] = { + notifications: mute.hide_notifications?, + } + end end def requested_map(target_account_ids, account_id) @@ -70,8 +74,13 @@ module AccountInteractions block_relationships.find_or_create_by!(target_account: other_account) end - def mute!(other_account) - mute_relationships.find_or_create_by!(target_account: other_account) + def mute!(other_account, notifications: nil) + notifications = true if notifications.nil? + mute = mute_relationships.create_with(hide_notifications: notifications).find_or_create_by!(target_account: other_account) + # When toggling a mute between hiding and allowing notifications, the mute will already exist, so the find_or_create_by! call will return the existing Mute without updating the hide_notifications attribute. Therefore, we check that hide_notifications? is what we want and set it if it isn't. + if mute.hide_notifications? != notifications + mute.update!(hide_notifications: notifications) + end end def mute_conversation!(conversation) @@ -127,6 +136,10 @@ module AccountInteractions conversation_mutes.where(conversation: conversation).exists? end + def muting_notifications?(other_account) + mute_relationships.where(target_account: other_account, hide_notifications: true).exists? + end + def requested?(other_account) follow_requests.where(target_account: other_account).exists? end diff --git a/app/models/mute.rb b/app/models/mute.rb index 4174a3523..105696da6 100644 --- a/app/models/mute.rb +++ b/app/models/mute.rb @@ -3,11 +3,12 @@ # # Table name: mutes # -# created_at :datetime not null -# updated_at :datetime not null -# account_id :bigint not null -# id :bigint not null, primary key -# target_account_id :bigint not null +# id :integer not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# target_account_id :integer not null +# hide_notifications :boolean default(TRUE), not null # class Mute < ApplicationRecord diff --git a/app/services/mute_service.rb b/app/services/mute_service.rb index 132369484..9b7cbd81f 100644 --- a/app/services/mute_service.rb +++ b/app/services/mute_service.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true class MuteService < BaseService - def call(account, target_account) + def call(account, target_account, notifications: nil) return if account.id == target_account.id - mute = account.mute!(target_account) + FeedManager.instance.clear_from_timeline(account, target_account) + mute = account.mute!(target_account, notifications: notifications) BlockWorker.perform_async(account.id, target_account.id) mute end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 6a24a8247..8a77f2f38 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -81,7 +81,7 @@ class NotifyService < BaseService blocked ||= from_self? # Skip for interactions with self blocked ||= domain_blocking? # Skip for domain blocked accounts blocked ||= @recipient.blocking?(@notification.from_account) # Skip for blocked accounts - blocked ||= @recipient.muting?(@notification.from_account) # Skip for muted accounts + blocked ||= @recipient.muting_notifications?(@notification.from_account) blocked ||= hellbanned? # Hellban blocked ||= optional_non_follower? # Options blocked ||= optional_non_following? # Options diff --git a/db/migrate/20170716191202_add_hide_notifications_to_mute.rb b/db/migrate/20170716191202_add_hide_notifications_to_mute.rb new file mode 100644 index 000000000..0410938c9 --- /dev/null +++ b/db/migrate/20170716191202_add_hide_notifications_to_mute.rb @@ -0,0 +1,15 @@ +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddHideNotificationsToMute < ActiveRecord::Migration[5.1] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def up + add_column_with_default :mutes, :hide_notifications, :boolean, default: true, allow_null: false + end + + def down + remove_column :mutes, :hide_notifications + end +end diff --git a/db/schema.rb b/db/schema.rb index bf319ce55..2d763e2f4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -203,6 +203,7 @@ ActiveRecord::Schema.define(version: 20171114080328) do t.datetime "updated_at", null: false t.bigint "account_id", null: false t.bigint "target_account_id", null: false + t.boolean "hide_notifications", default: true, null: false t.index ["account_id", "target_account_id"], name: "index_mutes_on_account_id_and_target_account_id", unique: true end diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index c770649ec..053c53e5a 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -137,6 +137,35 @@ RSpec.describe Api::V1::AccountsController, type: :controller do it 'creates a muting relation' do expect(user.account.muting?(other_account)).to be true end + + it 'mutes notifications' do + expect(user.account.muting_notifications?(other_account)).to be true + end + end + + describe 'POST #mute with notifications set to false' do + let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + user.account.follow!(other_account) + post :mute, params: {id: other_account.id, notifications: false } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'does not remove the following relation between user and target user' do + expect(user.account.following?(other_account)).to be true + end + + it 'creates a muting relation' do + expect(user.account.muting?(other_account)).to be true + end + + it 'does not mute notifications' do + expect(user.account.muting_notifications?(other_account)).to be false + end end describe 'POST #unmute' do diff --git a/spec/controllers/api/v1/mutes_controller_spec.rb b/spec/controllers/api/v1/mutes_controller_spec.rb index 3e6fa887b..97d6c2773 100644 --- a/spec/controllers/api/v1/mutes_controller_spec.rb +++ b/spec/controllers/api/v1/mutes_controller_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Api::V1::MutesController, type: :controller do let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') } before do - Fabricate(:mute, account: user.account) + Fabricate(:mute, account: user.account, hide_notifications: false) allow(controller).to receive(:doorkeeper_token) { token } end diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb new file mode 100644 index 000000000..a468549d8 --- /dev/null +++ b/spec/models/concerns/account_interactions_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +describe AccountInteractions do + describe 'muting an account' do + let(:me) { Fabricate(:account, username: 'Me') } + let(:you) { Fabricate(:account, username: 'You') } + + context 'with the notifications option unspecified' do + before do + me.mute!(you) + end + + it 'defaults to muting notifications' do + expect(me.muting_notifications?(you)).to be true + end + end + + context 'with the notifications option set to false' do + before do + me.mute!(you, notifications: false) + end + + it 'does not mute notifications' do + expect(me.muting_notifications?(you)).to be false + end + end + + context 'with the notifications option set to true' do + before do + me.mute!(you, notifications: true) + end + + it 'does mute notifications' do + expect(me.muting_notifications?(you)).to be true + end + end + end +end diff --git a/spec/services/mute_service_spec.rb b/spec/services/mute_service_spec.rb index 8097cb250..800140b6f 100644 --- a/spec/services/mute_service_spec.rb +++ b/spec/services/mute_service_spec.rb @@ -32,4 +32,36 @@ RSpec.describe MuteService do account.muting?(target_account) }.from(false).to(true) end + + context 'without specifying a notifications parameter' do + it 'mutes notifications from the account' do + is_expected.to change { + account.muting_notifications?(target_account) + }.from(false).to(true) + end + end + + context 'with a true notifications parameter' do + subject do + -> { described_class.new.call(account, target_account, notifications: true) } + end + + it 'mutes notifications from the account' do + is_expected.to change { + account.muting_notifications?(target_account) + }.from(false).to(true) + end + end + + context 'with a false notifications parameter' do + subject do + -> { described_class.new.call(account, target_account, notifications: false) } + end + + it 'does not mute notifications from the account' do + is_expected.to_not change { + account.muting_notifications?(target_account) + }.from(false) + end + end end diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb index 58ee66ded..fad0dd369 100644 --- a/spec/services/notify_service_spec.rb +++ b/spec/services/notify_service_spec.rb @@ -17,6 +17,16 @@ RSpec.describe NotifyService do is_expected.to_not change(Notification, :count) end + it 'does not notify when sender is muted with hide_notifications' do + recipient.mute!(sender, notifications: true) + is_expected.to_not change(Notification, :count) + end + + it 'does notify when sender is muted without hide_notifications' do + recipient.mute!(sender, notifications: false) + is_expected.to change(Notification, :count) + end + it 'does not notify when sender\'s domain is blocked' do recipient.block_domain!(sender.domain) is_expected.to_not change(Notification, :count) -- cgit From 19e8b861a2c97240e0ca9c47d13bb3d7c5cb7520 Mon Sep 17 00:00:00 2001 From: ysksn Date: Thu, 16 Nov 2017 00:05:20 +0900 Subject: Delegate some methods of User to @settings (#5706) * Move some tests of User into Settings::ScopedSettings * Add a test for User@settings --- app/models/user.rb | 40 ++++--------------------------- spec/lib/settings/scoped_settings_spec.rb | 35 +++++++++++++++++++++++++++ spec/models/user_spec.rb | 39 +++--------------------------- 3 files changed, 42 insertions(+), 72 deletions(-) create mode 100644 spec/lib/settings/scoped_settings_spec.rb (limited to 'app/models') diff --git a/app/models/user.rb b/app/models/user.rb index ebe768c52..326b871a1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -73,6 +73,10 @@ class User < ApplicationRecord has_many :session_activations, dependent: :destroy + delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal, + :reduce_motion, :system_font_ui, :noindex, :theme, + to: :settings, prefix: :setting, allow_nil: false + def confirmed? confirmed_at.present? end @@ -136,42 +140,6 @@ class User < ApplicationRecord settings.default_privacy || (account.locked? ? 'private' : 'public') end - def setting_default_sensitive - settings.default_sensitive - end - - def setting_unfollow_modal - settings.unfollow_modal - end - - def setting_boost_modal - settings.boost_modal - end - - def setting_delete_modal - settings.delete_modal - end - - def setting_auto_play_gif - settings.auto_play_gif - end - - def setting_reduce_motion - settings.reduce_motion - end - - def setting_system_font_ui - settings.system_font_ui - end - - def setting_noindex - settings.noindex - end - - def setting_theme - settings.theme - end - def token_for_app(a) return nil if a.nil? || a.owner != self Doorkeeper::AccessToken diff --git a/spec/lib/settings/scoped_settings_spec.rb b/spec/lib/settings/scoped_settings_spec.rb new file mode 100644 index 000000000..7566685b4 --- /dev/null +++ b/spec/lib/settings/scoped_settings_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Settings::ScopedSettings do + let(:object) { Fabricate(:user) } + let(:scoped_setting) { described_class.new(object) } + let(:val) { 'whatever' } + let(:methods) { %i(auto_play_gif default_sensitive unfollow_modal boost_modal delete_modal reduce_motion system_font_ui noindex theme) } + + describe '.initialize' do + it 'sets @object' do + scoped_setting = described_class.new(object) + expect(scoped_setting.instance_variable_get(:@object)).to be object + end + end + + describe '#method_missing' do + it 'sets scoped_setting.method_name = val' do + methods.each do |key| + scoped_setting.send("#{key}=", val) + expect(scoped_setting.send(key)).to eq val + end + end + end + + describe '#[]= and #[]' do + it 'sets [key] = val' do + methods.each do |key| + scoped_setting[key] = val + expect(scoped_setting[key]).to eq val + end + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 99aeca01b..77a12c26d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -177,27 +177,10 @@ RSpec.describe User, type: :model do end end - describe '#setting_auto_play_gif' do - it 'returns auto-play gif setting' do + describe 'settings' do + it 'is instance of Settings::ScopedSettings' do user = Fabricate(:user) - user.settings[:auto_play_gif] = false - expect(user.setting_auto_play_gif).to eq false - end - end - - describe '#setting_system_font_ui' do - it 'returns system font ui setting' do - user = Fabricate(:user) - user.settings[:system_font_ui] = false - expect(user.setting_system_font_ui).to eq false - end - end - - describe '#setting_boost_modal' do - it 'returns boost modal setting' do - user = Fabricate(:user) - user.settings[:boost_modal] = false - expect(user.setting_boost_modal).to eq false + expect(user.settings).to be_kind_of Settings::ScopedSettings end end @@ -219,22 +202,6 @@ RSpec.describe User, type: :model do end end - describe '#setting_unfollow_modal' do - it 'returns unfollow modal setting' do - user = Fabricate(:user) - user.settings[:unfollow_modal] = true - expect(user.setting_unfollow_modal).to eq true - end - end - - describe '#setting_delete_modal' do - it 'returns delete modal setting' do - user = Fabricate(:user) - user.settings[:delete_modal] = false - expect(user.setting_delete_modal).to eq false - end - end - describe 'whitelist' do around(:each) do |example| old_whitelist = Rails.configuration.x.email_whitelist -- cgit