Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `app/controllers/settings/preferences_controller.rb`: Upstream dropping `digest` from notifications emails while we have more notification emails settings. Removed `digest` from our list while keeping our extra settings. - `app/javascript/packs/admin.js`: Conflicts caused by glitch-soc's theming system. Applied the changes to `app/javascript/core/admin.js`. - `app/views/settings/preferences/other/show.html.haml`: Upstream removed a setting close to a glitch-soc-only setting. Applied upstream's change.
This commit is contained in:
commit
077183a121
10
Gemfile.lock
10
Gemfile.lock
@ -75,8 +75,8 @@ GEM
|
|||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
zeitwerk (~> 2.3)
|
zeitwerk (~> 2.3)
|
||||||
addressable (2.8.0)
|
addressable (2.8.1)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
aes_key_wrap (1.1.0)
|
aes_key_wrap (1.1.0)
|
||||||
airbrussh (1.4.1)
|
airbrussh (1.4.1)
|
||||||
sshkit (>= 1.6.1, != 1.7.0)
|
sshkit (>= 1.6.1, != 1.7.0)
|
||||||
@ -424,7 +424,7 @@ GEM
|
|||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sidekiq (>= 3.5)
|
sidekiq (>= 3.5)
|
||||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||||
oj (3.13.20)
|
oj (3.13.21)
|
||||||
omniauth (1.9.2)
|
omniauth (1.9.2)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 1.6.2, < 3)
|
rack (>= 1.6.2, < 3)
|
||||||
@ -480,8 +480,8 @@ GEM
|
|||||||
pry (>= 0.13, < 0.15)
|
pry (>= 0.13, < 0.15)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.7)
|
public_suffix (5.0.0)
|
||||||
puma (5.6.4)
|
puma (5.6.5)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.2.0)
|
pundit (2.2.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
@ -16,7 +16,11 @@ module Admin
|
|||||||
def batch
|
def batch
|
||||||
authorize :account, :index?
|
authorize :account, :index?
|
||||||
|
|
||||||
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
|
@form = Form::AccountBatch.new(form_account_batch_params)
|
||||||
|
@form.current_account = current_account
|
||||||
|
@form.action = action_from_button
|
||||||
|
@form.select_all_matching = params[:select_all_matching]
|
||||||
|
@form.query = filtered_accounts
|
||||||
@form.save
|
@form.save
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||||
|
@ -23,6 +23,7 @@ module Admin
|
|||||||
@role.current_account = current_account
|
@role.current_account = current_account
|
||||||
|
|
||||||
if @role.save
|
if @role.save
|
||||||
|
log_action :create, @role
|
||||||
redirect_to admin_roles_path
|
redirect_to admin_roles_path
|
||||||
else
|
else
|
||||||
render :new
|
render :new
|
||||||
@ -39,6 +40,7 @@ module Admin
|
|||||||
@role.current_account = current_account
|
@role.current_account = current_account
|
||||||
|
|
||||||
if @role.update(resource_params)
|
if @role.update(resource_params)
|
||||||
|
log_action :update, @role
|
||||||
redirect_to admin_roles_path
|
redirect_to admin_roles_path
|
||||||
else
|
else
|
||||||
render :edit
|
render :edit
|
||||||
@ -48,6 +50,7 @@ module Admin
|
|||||||
def destroy
|
def destroy
|
||||||
authorize @role, :destroy?
|
authorize @role, :destroy?
|
||||||
@role.destroy!
|
@role.destroy!
|
||||||
|
log_action :destroy, @role
|
||||||
redirect_to admin_roles_path
|
redirect_to admin_roles_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ module Admin
|
|||||||
@user.current_account = current_account
|
@user.current_account = current_account
|
||||||
|
|
||||||
if @user.update(resource_params)
|
if @user.update(resource_params)
|
||||||
|
log_action :change_role, @user
|
||||||
redirect_to admin_account_path(@user.account_id), notice: I18n.t('admin.accounts.change_role.changed_msg')
|
redirect_to admin_account_path(@user.account_id), notice: I18n.t('admin.accounts.change_role.changed_msg')
|
||||||
else
|
else
|
||||||
render :show
|
render :show
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
include AccountableConcern
|
||||||
|
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:canonical_email_blocks' }, only: [:index, :show, :test]
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:canonical_email_blocks' }, except: [:index, :show, :test]
|
||||||
|
|
||||||
|
before_action :set_canonical_email_blocks, only: :index
|
||||||
|
before_action :set_canonical_email_blocks_from_test, only: [:test]
|
||||||
|
before_action :set_canonical_email_block, only: [:show, :destroy]
|
||||||
|
|
||||||
|
after_action :verify_authorized
|
||||||
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
PAGINATION_PARAMS = %i(limit).freeze
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :canonical_email_block, :index?
|
||||||
|
render json: @canonical_email_blocks, each_serializer: REST::Admin::CanonicalEmailBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @canonical_email_block, :show?
|
||||||
|
render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def test
|
||||||
|
authorize :canonical_email_block, :test?
|
||||||
|
render json: @canonical_email_blocks, each_serializer: REST::Admin::CanonicalEmailBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize :canonical_email_block, :create?
|
||||||
|
|
||||||
|
@canonical_email_block = CanonicalEmailBlock.create!(resource_params)
|
||||||
|
log_action :create, @canonical_email_block
|
||||||
|
|
||||||
|
render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @canonical_email_block, :destroy?
|
||||||
|
|
||||||
|
@canonical_email_block.destroy!
|
||||||
|
log_action :destroy, @canonical_email_block
|
||||||
|
|
||||||
|
render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(:canonical_email_hash, :email)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_canonical_email_blocks
|
||||||
|
@canonical_email_blocks = CanonicalEmailBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_canonical_email_blocks_from_test
|
||||||
|
@canonical_email_blocks = CanonicalEmailBlock.matching_email(params[:email])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_canonical_email_block
|
||||||
|
@canonical_email_block = CanonicalEmailBlock.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
api_v1_admin_canonical_email_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_admin_canonical_email_blocks_url(pagination_params(min_id: pagination_since_id)) unless @canonical_email_blocks.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@canonical_email_blocks.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@canonical_email_blocks.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@canonical_email_blocks.size == limit_param(LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,90 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
include AccountableConcern
|
||||||
|
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:email_domain_blocks' }, only: [:index, :show]
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:email_domain_blocks' }, except: [:index, :show]
|
||||||
|
before_action :set_email_domain_blocks, only: :index
|
||||||
|
before_action :set_email_domain_block, only: [:show, :destroy]
|
||||||
|
|
||||||
|
after_action :verify_authorized
|
||||||
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
PAGINATION_PARAMS = %i(
|
||||||
|
limit
|
||||||
|
).freeze
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize :email_domain_block, :create?
|
||||||
|
|
||||||
|
@email_domain_block = EmailDomainBlock.create!(resource_params)
|
||||||
|
log_action :create, @email_domain_block
|
||||||
|
|
||||||
|
render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :email_domain_block, :index?
|
||||||
|
render json: @email_domain_blocks, each_serializer: REST::Admin::EmailDomainBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @email_domain_block, :show?
|
||||||
|
render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @email_domain_block, :destroy?
|
||||||
|
|
||||||
|
@email_domain_block.destroy!
|
||||||
|
log_action :destroy, @email_domain_block
|
||||||
|
|
||||||
|
render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_email_domain_blocks
|
||||||
|
@email_domain_blocks = EmailDomainBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_email_domain_block
|
||||||
|
@email_domain_block = EmailDomainBlock.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(:domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
api_v1_admin_email_domain_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_admin_email_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @email_domain_blocks.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@email_domain_blocks.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@email_domain_blocks.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@email_domain_blocks.size == limit_param(LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
|
||||||
|
end
|
||||||
|
end
|
99
app/controllers/api/v1/admin/ip_blocks_controller.rb
Normal file
99
app/controllers/api/v1/admin/ip_blocks_controller.rb
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::IpBlocksController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
include AccountableConcern
|
||||||
|
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:ip_blocks' }, only: [:index, :show]
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:ip_blocks' }, except: [:index, :show]
|
||||||
|
before_action :set_ip_blocks, only: :index
|
||||||
|
before_action :set_ip_block, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
|
after_action :verify_authorized
|
||||||
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
PAGINATION_PARAMS = %i(
|
||||||
|
limit
|
||||||
|
).freeze
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize :ip_block, :create?
|
||||||
|
|
||||||
|
@ip_block = IpBlock.create!(resource_params)
|
||||||
|
log_action :create, @ip_block
|
||||||
|
|
||||||
|
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :ip_block, :index?
|
||||||
|
render json: @ip_blocks, each_serializer: REST::Admin::IpBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @ip_block, :show?
|
||||||
|
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize @ip_block, :update?
|
||||||
|
|
||||||
|
@ip_block.update(resource_params)
|
||||||
|
log_action :update, @ip_block
|
||||||
|
|
||||||
|
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @ip_block, :destroy?
|
||||||
|
|
||||||
|
@ip_block.destroy!
|
||||||
|
log_action :destroy, @ip_block
|
||||||
|
|
||||||
|
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_ip_blocks
|
||||||
|
@ip_blocks = IpBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_ip_block
|
||||||
|
@ip_block = IpBlock.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(:ip, :severity, :comment, :expires_in)
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
api_v1_admin_ip_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_admin_ip_blocks_url(pagination_params(min_id: pagination_since_id)) unless @ip_blocks.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@ip_blocks.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@ip_blocks.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@ip_blocks.size == limit_param(LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
|
||||||
|
end
|
||||||
|
end
|
@ -3,7 +3,11 @@
|
|||||||
module AccountableConcern
|
module AccountableConcern
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
def log_action(action, target, options = {})
|
def log_action(action, target)
|
||||||
Admin::ActionLog.create(account: current_account, action: action, target: target, recorded_changes: options.stringify_keys)
|
Admin::ActionLog.create(
|
||||||
|
account: current_account,
|
||||||
|
action: action,
|
||||||
|
target: target
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -58,7 +58,7 @@ class Settings::PreferencesController < Settings::BaseController
|
|||||||
:setting_trends,
|
:setting_trends,
|
||||||
:setting_crop_images,
|
:setting_crop_images,
|
||||||
:setting_always_send_emails,
|
:setting_always_send_emails,
|
||||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag trending_link trending_status appeal),
|
notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag trending_link trending_status appeal),
|
||||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -2,64 +2,29 @@
|
|||||||
|
|
||||||
module Admin::ActionLogsHelper
|
module Admin::ActionLogsHelper
|
||||||
def log_target(log)
|
def log_target(log)
|
||||||
if log.target
|
case log.target_type
|
||||||
linkable_log_target(log.target)
|
|
||||||
else
|
|
||||||
log_target_from_history(log.target_type, log.recorded_changes)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def linkable_log_target(record)
|
|
||||||
case record.class.name
|
|
||||||
when 'Account'
|
when 'Account'
|
||||||
link_to record.acct, admin_account_path(record.id)
|
link_to log.human_identifier, admin_account_path(log.target_id)
|
||||||
when 'User'
|
when 'User'
|
||||||
link_to record.account.acct, admin_account_path(record.account_id)
|
link_to log.human_identifier, admin_account_path(log.route_param)
|
||||||
when 'CustomEmoji'
|
when 'UserRole'
|
||||||
record.shortcode
|
link_to log.human_identifier, admin_roles_path(log.target_id)
|
||||||
when 'Report'
|
when 'Report'
|
||||||
link_to "##{record.id}", admin_report_path(record)
|
link_to "##{log.human_identifier}", admin_report_path(log.target_id)
|
||||||
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
|
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
|
||||||
link_to record.domain, "https://#{record.domain}"
|
link_to log.human_identifier, "https://#{log.human_identifier}"
|
||||||
when 'Status'
|
when 'Status'
|
||||||
link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record)
|
link_to log.human_identifier, log.permalink
|
||||||
when 'AccountWarning'
|
when 'AccountWarning'
|
||||||
link_to record.target_account.acct, admin_account_path(record.target_account_id)
|
link_to log.human_identifier, admin_account_path(log.target_id)
|
||||||
when 'Announcement'
|
when 'Announcement'
|
||||||
link_to truncate(record.text), edit_admin_announcement_path(record.id)
|
link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id)
|
||||||
when 'IpBlock'
|
when 'IpBlock', 'Instance', 'CustomEmoji'
|
||||||
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
|
log.human_identifier
|
||||||
when 'Instance'
|
when 'CanonicalEmailBlock'
|
||||||
record.domain
|
content_tag(:samp, log.human_identifier[0...7], title: log.human_identifier)
|
||||||
when 'Appeal'
|
when 'Appeal'
|
||||||
link_to record.account.acct, disputes_strike_path(record.strike)
|
link_to log.human_identifier, disputes_strike_path(log.route_param)
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def log_target_from_history(type, attributes)
|
|
||||||
case type
|
|
||||||
when 'User'
|
|
||||||
attributes['username']
|
|
||||||
when 'CustomEmoji'
|
|
||||||
attributes['shortcode']
|
|
||||||
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
|
|
||||||
link_to attributes['domain'], "https://#{attributes['domain']}"
|
|
||||||
when 'Status'
|
|
||||||
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))
|
|
||||||
|
|
||||||
if tmp_status.account
|
|
||||||
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id)
|
|
||||||
else
|
|
||||||
I18n.t('admin.action_logs.deleted_status')
|
|
||||||
end
|
|
||||||
when 'Announcement'
|
|
||||||
truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text'])
|
|
||||||
when 'IpBlock'
|
|
||||||
"#{attributes['ip']}/#{attributes['ip'].prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{attributes['severity']}")})"
|
|
||||||
when 'Instance'
|
|
||||||
attributes['domain']
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -6,18 +6,71 @@ import ready from '../mastodon/ready';
|
|||||||
|
|
||||||
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
|
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
|
||||||
|
|
||||||
|
const showSelectAll = () => {
|
||||||
|
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
|
||||||
|
selectAllMatchingElement.classList.add('active');
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideSelectAll = () => {
|
||||||
|
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
|
||||||
|
const hiddenField = document.querySelector('#select_all_matching');
|
||||||
|
const selectedMsg = document.querySelector('.batch-table__select-all .selected');
|
||||||
|
const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
|
||||||
|
|
||||||
|
selectAllMatchingElement.classList.remove('active');
|
||||||
|
selectedMsg.classList.remove('active');
|
||||||
|
notSelectedMsg.classList.add('active');
|
||||||
|
hiddenField.value = '0';
|
||||||
|
};
|
||||||
|
|
||||||
delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
|
delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
|
||||||
|
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
|
||||||
|
|
||||||
[].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => {
|
[].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => {
|
||||||
content.checked = target.checked;
|
content.checked = target.checked;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (selectAllMatchingElement) {
|
||||||
|
if (target.checked) {
|
||||||
|
showSelectAll();
|
||||||
|
} else {
|
||||||
|
hideSelectAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
delegate(document, '.batch-table__select-all button', 'click', () => {
|
||||||
|
const hiddenField = document.querySelector('#select_all_matching');
|
||||||
|
const active = hiddenField.value === '1';
|
||||||
|
const selectedMsg = document.querySelector('.batch-table__select-all .selected');
|
||||||
|
const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
hiddenField.value = '0';
|
||||||
|
selectedMsg.classList.remove('active');
|
||||||
|
notSelectedMsg.classList.add('active');
|
||||||
|
} else {
|
||||||
|
hiddenField.value = '1';
|
||||||
|
notSelectedMsg.classList.remove('active');
|
||||||
|
selectedMsg.classList.add('active');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, batchCheckboxClassName, 'change', () => {
|
delegate(document, batchCheckboxClassName, 'change', () => {
|
||||||
const checkAllElement = document.querySelector('#batch_checkbox_all');
|
const checkAllElement = document.querySelector('#batch_checkbox_all');
|
||||||
|
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
|
||||||
|
|
||||||
if (checkAllElement) {
|
if (checkAllElement) {
|
||||||
checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
|
checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
|
||||||
checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
|
checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
|
||||||
|
|
||||||
|
if (selectAllMatchingElement) {
|
||||||
|
if (checkAllElement.checked) {
|
||||||
|
showSelectAll();
|
||||||
|
} else {
|
||||||
|
hideSelectAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ const TimelineHint = ({ resource, url }) => (
|
|||||||
<div className='timeline-hint'>
|
<div className='timeline-hint'>
|
||||||
<strong><FormattedMessage id='timeline_hint.remote_resource_not_displayed' defaultMessage='{resource} from other servers are not displayed.' values={{ resource }} /></strong>
|
<strong><FormattedMessage id='timeline_hint.remote_resource_not_displayed' defaultMessage='{resource} from other servers are not displayed.' values={{ resource }} /></strong>
|
||||||
<br />
|
<br />
|
||||||
<a href={url} target='_blank'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a>
|
<a href={url} target='_blank' rel='noopener'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import * as registerPushNotifications from './actions/push_notifications';
|
|
||||||
import { setupBrowserNotifications } from './actions/notifications';
|
|
||||||
import { default as Mastodon, store } from './containers/mastodon';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import ready from './ready';
|
import * as registerPushNotifications from 'mastodon/actions/push_notifications';
|
||||||
|
import { setupBrowserNotifications } from 'mastodon/actions/notifications';
|
||||||
|
import Mastodon, { store } from 'mastodon/containers/mastodon';
|
||||||
|
import ready from 'mastodon/ready';
|
||||||
|
|
||||||
const perf = require('./performance');
|
const perf = require('./performance');
|
||||||
|
|
||||||
@ -24,10 +24,20 @@ function main() {
|
|||||||
|
|
||||||
ReactDOM.render(<Mastodon {...props} />, mountNode);
|
ReactDOM.render(<Mastodon {...props} />, mountNode);
|
||||||
store.dispatch(setupBrowserNotifications());
|
store.dispatch(setupBrowserNotifications());
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
// avoid offline in dev mode because it's harder to debug
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
require('offline-plugin/runtime').install();
|
import('workbox-window')
|
||||||
|
.then(({ Workbox }) => {
|
||||||
|
const wb = new Workbox('/sw.js');
|
||||||
|
|
||||||
|
return wb.register();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
store.dispatch(registerPushNotifications.register());
|
store.dispatch(registerPushNotifications.register());
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
perf.stop('main()');
|
perf.stop('main()');
|
||||||
});
|
});
|
||||||
|
@ -1,20 +1,59 @@
|
|||||||
// import { freeStorage, storageFreeable } from '../storage/modifier';
|
import { ExpirationPlugin } from 'workbox-expiration';
|
||||||
import './web_push_notifications';
|
import { precacheAndRoute } from 'workbox-precaching';
|
||||||
|
import { registerRoute } from 'workbox-routing';
|
||||||
|
import { CacheFirst } from 'workbox-strategies';
|
||||||
|
import { handleNotificationClick, handlePush } from './web_push_notifications';
|
||||||
|
|
||||||
// function openSystemCache() {
|
const CACHE_NAME_PREFIX = 'mastodon-';
|
||||||
// return caches.open('mastodon-system');
|
|
||||||
// }
|
|
||||||
|
|
||||||
function openWebCache() {
|
function openWebCache() {
|
||||||
return caches.open('mastodon-web');
|
return caches.open(`${CACHE_NAME_PREFIX}web`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchRoot() {
|
function fetchRoot() {
|
||||||
return fetch('/', { credentials: 'include', redirect: 'manual' });
|
return fetch('/', { credentials: 'include', redirect: 'manual' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// const firefox = navigator.userAgent.match(/Firefox\/(\d+)/);
|
precacheAndRoute(self.__WB_MANIFEST);
|
||||||
// const invalidOnlyIfCached = firefox && firefox[1] < 60;
|
|
||||||
|
registerRoute(
|
||||||
|
/locale_.*\.js$/,
|
||||||
|
new CacheFirst({
|
||||||
|
cacheName: `${CACHE_NAME_PREFIX}locales`,
|
||||||
|
plugins: [
|
||||||
|
new ExpirationPlugin({
|
||||||
|
maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month
|
||||||
|
maxEntries: 5,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
registerRoute(
|
||||||
|
({ request }) => request.destination === 'font',
|
||||||
|
new CacheFirst({
|
||||||
|
cacheName: `${CACHE_NAME_PREFIX}fonts`,
|
||||||
|
plugins: [
|
||||||
|
new ExpirationPlugin({
|
||||||
|
maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month
|
||||||
|
maxEntries: 5,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
registerRoute(
|
||||||
|
({ request }) => ['audio', 'image', 'track', 'video'].includes(request.destination),
|
||||||
|
new CacheFirst({
|
||||||
|
cacheName: `m${CACHE_NAME_PREFIX}media`,
|
||||||
|
plugins: [
|
||||||
|
new ExpirationPlugin({
|
||||||
|
maxAgeSeconds: 7 * 24 * 60 * 60, // 1 week
|
||||||
|
maxEntries: 256,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Cause a new version of a registered Service Worker to replace an existing one
|
// Cause a new version of a registered Service Worker to replace an existing one
|
||||||
// that is already installed, and replace the currently active worker on open pages.
|
// that is already installed, and replace the currently active worker on open pages.
|
||||||
@ -52,26 +91,8 @@ self.addEventListener('fetch', function(event) {
|
|||||||
|
|
||||||
return response;
|
return response;
|
||||||
}));
|
}));
|
||||||
} /* else if (storageFreeable && (ATTACHMENT_HOST ? url.host === ATTACHMENT_HOST : url.pathname.startsWith('/system/'))) {
|
|
||||||
event.respondWith(openSystemCache().then(cache => {
|
|
||||||
return cache.match(event.request.url).then(cached => {
|
|
||||||
if (cached === undefined) {
|
|
||||||
const asyncResponse = invalidOnlyIfCached && event.request.cache === 'only-if-cached' ?
|
|
||||||
fetch(event.request, { cache: 'no-cache' }) : fetch(event.request);
|
|
||||||
|
|
||||||
return asyncResponse.then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
cache
|
|
||||||
.put(event.request.url, response.clone())
|
|
||||||
.catch(()=>{}).then(freeStorage()).catch();
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return response;
|
self.addEventListener('push', handlePush);
|
||||||
});
|
self.addEventListener('notificationclick', handleNotificationClick);
|
||||||
}
|
|
||||||
|
|
||||||
return cached;
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
} */
|
|
||||||
});
|
|
||||||
|
@ -75,7 +75,7 @@ const formatMessage = (messageId, locale, values = {}) =>
|
|||||||
const htmlToPlainText = html =>
|
const htmlToPlainText = html =>
|
||||||
unescape(html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, ''));
|
unescape(html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, ''));
|
||||||
|
|
||||||
const handlePush = (event) => {
|
export const handlePush = (event) => {
|
||||||
const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json();
|
const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json();
|
||||||
|
|
||||||
// Placeholder until more information can be loaded
|
// Placeholder until more information can be loaded
|
||||||
@ -189,7 +189,7 @@ const openUrl = url =>
|
|||||||
return self.clients.openWindow(url);
|
return self.clients.openWindow(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleNotificationClick = (event) => {
|
export const handleNotificationClick = (event) => {
|
||||||
const reactToNotificationClick = new Promise((resolve, reject) => {
|
const reactToNotificationClick = new Promise((resolve, reject) => {
|
||||||
if (event.action) {
|
if (event.action) {
|
||||||
if (event.action === 'expand') {
|
if (event.action === 'expand') {
|
||||||
@ -211,6 +211,3 @@ const handleNotificationClick = (event) => {
|
|||||||
|
|
||||||
event.waitUntil(reactToNotificationClick);
|
event.waitUntil(reactToNotificationClick);
|
||||||
};
|
};
|
||||||
|
|
||||||
self.addEventListener('push', handlePush);
|
|
||||||
self.addEventListener('notificationclick', handleNotificationClick);
|
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
export default () => new Promise((resolve, reject) => {
|
|
||||||
// ServiceWorker is required to synchronize the login state.
|
|
||||||
// Microsoft Edge 17 does not support getAll according to:
|
|
||||||
// Catalog of standard and vendor APIs across browsers - Microsoft Edge Development
|
|
||||||
// https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?q=specName%3Aindexeddb
|
|
||||||
if (!('caches' in self && 'getAll' in IDBObjectStore.prototype)) {
|
|
||||||
reject();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = indexedDB.open('mastodon');
|
|
||||||
|
|
||||||
request.onerror = reject;
|
|
||||||
request.onsuccess = ({ target }) => resolve(target.result);
|
|
||||||
|
|
||||||
request.onupgradeneeded = ({ target }) => {
|
|
||||||
const accounts = target.result.createObjectStore('accounts', { autoIncrement: true });
|
|
||||||
const statuses = target.result.createObjectStore('statuses', { autoIncrement: true });
|
|
||||||
|
|
||||||
accounts.createIndex('id', 'id', { unique: true });
|
|
||||||
accounts.createIndex('moved', 'moved');
|
|
||||||
|
|
||||||
statuses.createIndex('id', 'id', { unique: true });
|
|
||||||
statuses.createIndex('account', 'account');
|
|
||||||
statuses.createIndex('reblog', 'reblog');
|
|
||||||
};
|
|
||||||
});
|
|
@ -1,211 +0,0 @@
|
|||||||
import openDB from './db';
|
|
||||||
|
|
||||||
const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static'];
|
|
||||||
const storageMargin = 8388608;
|
|
||||||
const storeLimit = 1024;
|
|
||||||
|
|
||||||
// navigator.storage is not present on:
|
|
||||||
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299
|
|
||||||
// estimate method is not present on Chrome 57.0.2987.98 on Linux.
|
|
||||||
export const storageFreeable = 'storage' in navigator && 'estimate' in navigator.storage;
|
|
||||||
|
|
||||||
function openCache() {
|
|
||||||
// ServiceWorker and Cache API is not available on iOS 11
|
|
||||||
// https://webkit.org/status/#specification-service-workers
|
|
||||||
return self.caches ? caches.open('mastodon-system') : Promise.reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
function printErrorIfAvailable(error) {
|
|
||||||
if (error) {
|
|
||||||
console.warn(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function put(name, objects, onupdate, oncreate) {
|
|
||||||
return openDB().then(db => (new Promise((resolve, reject) => {
|
|
||||||
const putTransaction = db.transaction(name, 'readwrite');
|
|
||||||
const putStore = putTransaction.objectStore(name);
|
|
||||||
const putIndex = putStore.index('id');
|
|
||||||
|
|
||||||
objects.forEach(object => {
|
|
||||||
putIndex.getKey(object.id).onsuccess = retrieval => {
|
|
||||||
function addObject() {
|
|
||||||
putStore.add(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteObject() {
|
|
||||||
putStore.delete(retrieval.target.result).onsuccess = addObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retrieval.target.result) {
|
|
||||||
if (onupdate) {
|
|
||||||
onupdate(object, retrieval.target.result, putStore, deleteObject);
|
|
||||||
} else {
|
|
||||||
deleteObject();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (oncreate) {
|
|
||||||
oncreate(object, addObject);
|
|
||||||
} else {
|
|
||||||
addObject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
putTransaction.oncomplete = () => {
|
|
||||||
const readTransaction = db.transaction(name, 'readonly');
|
|
||||||
const readStore = readTransaction.objectStore(name);
|
|
||||||
const count = readStore.count();
|
|
||||||
|
|
||||||
count.onsuccess = () => {
|
|
||||||
const excess = count.result - storeLimit;
|
|
||||||
|
|
||||||
if (excess > 0) {
|
|
||||||
const retrieval = readStore.getAll(null, excess);
|
|
||||||
|
|
||||||
retrieval.onsuccess = () => resolve(retrieval.result);
|
|
||||||
retrieval.onerror = reject;
|
|
||||||
} else {
|
|
||||||
resolve([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
count.onerror = reject;
|
|
||||||
};
|
|
||||||
|
|
||||||
putTransaction.onerror = reject;
|
|
||||||
})).then(resolved => {
|
|
||||||
db.close();
|
|
||||||
return resolved;
|
|
||||||
}, error => {
|
|
||||||
db.close();
|
|
||||||
throw error;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function evictAccountsByRecords(records) {
|
|
||||||
return openDB().then(db => {
|
|
||||||
const transaction = db.transaction(['accounts', 'statuses'], 'readwrite');
|
|
||||||
const accounts = transaction.objectStore('accounts');
|
|
||||||
const accountsIdIndex = accounts.index('id');
|
|
||||||
const accountsMovedIndex = accounts.index('moved');
|
|
||||||
const statuses = transaction.objectStore('statuses');
|
|
||||||
const statusesIndex = statuses.index('account');
|
|
||||||
|
|
||||||
function evict(toEvict) {
|
|
||||||
toEvict.forEach(record => {
|
|
||||||
openCache()
|
|
||||||
.then(cache => accountAssetKeys.forEach(key => cache.delete(records[key])))
|
|
||||||
.catch(printErrorIfAvailable);
|
|
||||||
|
|
||||||
accountsMovedIndex.getAll(record.id).onsuccess = ({ target }) => evict(target.result);
|
|
||||||
|
|
||||||
statusesIndex.getAll(record.id).onsuccess =
|
|
||||||
({ target }) => evictStatusesByRecords(target.result);
|
|
||||||
|
|
||||||
accountsIdIndex.getKey(record.id).onsuccess =
|
|
||||||
({ target }) => target.result && accounts.delete(target.result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
evict(records);
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
}).catch(printErrorIfAvailable);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function evictStatus(id) {
|
|
||||||
evictStatuses([id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function evictStatuses(ids) {
|
|
||||||
return openDB().then(db => {
|
|
||||||
const transaction = db.transaction('statuses', 'readwrite');
|
|
||||||
const store = transaction.objectStore('statuses');
|
|
||||||
const idIndex = store.index('id');
|
|
||||||
const reblogIndex = store.index('reblog');
|
|
||||||
|
|
||||||
ids.forEach(id => {
|
|
||||||
reblogIndex.getAllKeys(id).onsuccess =
|
|
||||||
({ target }) => target.result.forEach(reblogKey => store.delete(reblogKey));
|
|
||||||
|
|
||||||
idIndex.getKey(id).onsuccess =
|
|
||||||
({ target }) => target.result && store.delete(target.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
}).catch(printErrorIfAvailable);
|
|
||||||
}
|
|
||||||
|
|
||||||
function evictStatusesByRecords(records) {
|
|
||||||
return evictStatuses(records.map(({ id }) => id));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function putAccounts(records, avatarStatic) {
|
|
||||||
const avatarKey = avatarStatic ? 'avatar_static' : 'avatar';
|
|
||||||
const newURLs = [];
|
|
||||||
|
|
||||||
put('accounts', records, (newRecord, oldKey, store, oncomplete) => {
|
|
||||||
store.get(oldKey).onsuccess = ({ target }) => {
|
|
||||||
accountAssetKeys.forEach(key => {
|
|
||||||
const newURL = newRecord[key];
|
|
||||||
const oldURL = target.result[key];
|
|
||||||
|
|
||||||
if (newURL !== oldURL) {
|
|
||||||
openCache()
|
|
||||||
.then(cache => cache.delete(oldURL))
|
|
||||||
.catch(printErrorIfAvailable);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const newURL = newRecord[avatarKey];
|
|
||||||
const oldURL = target.result[avatarKey];
|
|
||||||
|
|
||||||
if (newURL !== oldURL) {
|
|
||||||
newURLs.push(newURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
oncomplete();
|
|
||||||
};
|
|
||||||
}, (newRecord, oncomplete) => {
|
|
||||||
newURLs.push(newRecord[avatarKey]);
|
|
||||||
oncomplete();
|
|
||||||
}).then(records => Promise.all([
|
|
||||||
evictAccountsByRecords(records),
|
|
||||||
openCache().then(cache => cache.addAll(newURLs)),
|
|
||||||
])).then(freeStorage, error => {
|
|
||||||
freeStorage();
|
|
||||||
throw error;
|
|
||||||
}).catch(printErrorIfAvailable);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function putStatuses(records) {
|
|
||||||
put('statuses', records)
|
|
||||||
.then(evictStatusesByRecords)
|
|
||||||
.catch(printErrorIfAvailable);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function freeStorage() {
|
|
||||||
return storageFreeable && navigator.storage.estimate().then(({ quota, usage }) => {
|
|
||||||
if (usage + storageMargin < quota) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return openDB().then(db => new Promise((resolve, reject) => {
|
|
||||||
const retrieval = db.transaction('accounts', 'readonly').objectStore('accounts').getAll(null, 1);
|
|
||||||
|
|
||||||
retrieval.onsuccess = () => {
|
|
||||||
if (retrieval.result.length > 0) {
|
|
||||||
resolve(evictAccountsByRecords(retrieval.result).then(freeStorage));
|
|
||||||
} else {
|
|
||||||
resolve(caches.delete('mastodon-system'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
retrieval.onerror = reject;
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
@ -190,6 +190,55 @@ a.table-action-link {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__select-all {
|
||||||
|
background: $ui-base-color;
|
||||||
|
height: 47px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid darken($ui-base-color, 8%);
|
||||||
|
border-top: 0;
|
||||||
|
color: $secondary-text-color;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected,
|
||||||
|
.not-selected {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding: 8px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
font: inherit;
|
||||||
|
color: $highlight-text-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background: lighten($ui-base-color, 8%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__form {
|
&__form {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: 1px solid darken($ui-base-color, 8%);
|
border: 1px solid darken($ui-base-color, 8%);
|
||||||
|
@ -60,7 +60,7 @@ class FeedManager
|
|||||||
# @param [Boolean] update
|
# @param [Boolean] update
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
def push_to_home(account, status, update: false)
|
def push_to_home(account, status, update: false)
|
||||||
return false unless add_to_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?)
|
return false unless add_to_feed(:home, account.id, status, aggregate_reblogs: true)
|
||||||
|
|
||||||
trim(:home, account.id)
|
trim(:home, account.id)
|
||||||
PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}", { 'update' => update }) if push_update_required?("timeline:#{account.id}")
|
PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}", { 'update' => update }) if push_update_required?("timeline:#{account.id}")
|
||||||
@ -73,7 +73,7 @@ class FeedManager
|
|||||||
# @param [Boolean] update
|
# @param [Boolean] update
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
def unpush_from_home(account, status, update: false)
|
def unpush_from_home(account, status, update: false)
|
||||||
return false unless remove_from_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?)
|
return false unless remove_from_feed(:home, account.id, status, aggregate_reblogs: true)
|
||||||
|
|
||||||
redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update
|
redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update
|
||||||
true
|
true
|
||||||
@ -85,7 +85,7 @@ class FeedManager
|
|||||||
# @param [Boolean] update
|
# @param [Boolean] update
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
def push_to_list(list, status, update: false)
|
def push_to_list(list, status, update: false)
|
||||||
return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?)
|
return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, aggregate_reblogs: true)
|
||||||
|
|
||||||
trim(:list, list.id)
|
trim(:list, list.id)
|
||||||
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}", { 'update' => update }) if push_update_required?("timeline:list:#{list.id}")
|
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}", { 'update' => update }) if push_update_required?("timeline:list:#{list.id}")
|
||||||
@ -98,7 +98,7 @@ class FeedManager
|
|||||||
# @param [Boolean] update
|
# @param [Boolean] update
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
def unpush_from_list(list, status, update: false)
|
def unpush_from_list(list, status, update: false)
|
||||||
return false unless remove_from_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?)
|
return false unless remove_from_feed(:list, list.id, status, aggregate_reblogs: true)
|
||||||
|
|
||||||
redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update
|
redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update
|
||||||
true
|
true
|
||||||
@ -133,7 +133,7 @@ class FeedManager
|
|||||||
# @return [void]
|
# @return [void]
|
||||||
def merge_into_home(from_account, into_account)
|
def merge_into_home(from_account, into_account)
|
||||||
timeline_key = key(:home, into_account.id)
|
timeline_key = key(:home, into_account.id)
|
||||||
aggregate = into_account.user&.aggregates_reblogs?
|
aggregate = true
|
||||||
query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
|
query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
|
||||||
|
|
||||||
if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
|
if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
|
||||||
@ -159,7 +159,7 @@ class FeedManager
|
|||||||
# @return [void]
|
# @return [void]
|
||||||
def merge_into_list(from_account, list)
|
def merge_into_list(from_account, list)
|
||||||
timeline_key = key(:list, list.id)
|
timeline_key = key(:list, list.id)
|
||||||
aggregate = list.account.user&.aggregates_reblogs?
|
aggregate = true
|
||||||
query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
|
query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
|
||||||
|
|
||||||
if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
|
if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
|
||||||
@ -188,7 +188,7 @@ class FeedManager
|
|||||||
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
|
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
|
||||||
|
|
||||||
from_account.statuses.select('id, reblog_of_id').where(id: timeline_status_ids).reorder(nil).find_each do |status|
|
from_account.statuses.select('id, reblog_of_id').where(id: timeline_status_ids).reorder(nil).find_each do |status|
|
||||||
remove_from_feed(:home, into_account.id, status, aggregate_reblogs: into_account.user&.aggregates_reblogs?)
|
remove_from_feed(:home, into_account.id, status, aggregate_reblogs: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -201,7 +201,7 @@ class FeedManager
|
|||||||
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
|
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
|
||||||
|
|
||||||
from_account.statuses.select('id, reblog_of_id').where(id: timeline_status_ids).reorder(nil).find_each do |status|
|
from_account.statuses.select('id, reblog_of_id').where(id: timeline_status_ids).reorder(nil).find_each do |status|
|
||||||
remove_from_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?)
|
remove_from_feed(:list, list.id, status, aggregate_reblogs: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -260,7 +260,7 @@ class FeedManager
|
|||||||
# @return [void]
|
# @return [void]
|
||||||
def populate_home(account)
|
def populate_home(account)
|
||||||
limit = FeedManager::MAX_ITEMS / 2
|
limit = FeedManager::MAX_ITEMS / 2
|
||||||
aggregate = account.user&.aggregates_reblogs?
|
aggregate = true
|
||||||
timeline_key = key(:home, account.id)
|
timeline_key = key(:home, account.id)
|
||||||
|
|
||||||
account.statuses.limit(limit).each do |status|
|
account.statuses.limit(limit).each do |status|
|
||||||
|
@ -66,24 +66,6 @@ class NotificationMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def digest(recipient, **opts)
|
|
||||||
return unless recipient.user.functional?
|
|
||||||
|
|
||||||
@me = recipient
|
|
||||||
@since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max
|
|
||||||
@notifications_count = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since).count
|
|
||||||
|
|
||||||
return if @notifications_count.zero?
|
|
||||||
|
|
||||||
@notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since).limit(40)
|
|
||||||
@follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count
|
|
||||||
|
|
||||||
locale_for_account(@me) do
|
|
||||||
mail to: @me.user.email,
|
|
||||||
subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications_count)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def thread_by_conversation(conversation)
|
def thread_by_conversation(conversation)
|
||||||
|
@ -364,6 +364,10 @@ class Account < ApplicationRecord
|
|||||||
username
|
username
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
acct
|
||||||
|
end
|
||||||
|
|
||||||
def excluded_from_timeline_account_ids
|
def excluded_from_timeline_account_ids
|
||||||
Rails.cache.fetch("exclude_account_ids_for:#{id}") { block_relationships.pluck(:target_account_id) + blocked_by_relationships.pluck(:account_id) + mute_relationships.pluck(:target_account_id) }
|
Rails.cache.fetch("exclude_account_ids_for:#{id}") { block_relationships.pluck(:target_account_id) + blocked_by_relationships.pluck(:account_id) + mute_relationships.pluck(:target_account_id) }
|
||||||
end
|
end
|
||||||
|
@ -43,4 +43,8 @@ class AccountWarning < ApplicationRecord
|
|||||||
def overruled?
|
def overruled?
|
||||||
overruled_at.present?
|
overruled_at.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
target_account.acct
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -9,38 +9,42 @@
|
|||||||
# action :string default(""), not null
|
# action :string default(""), not null
|
||||||
# target_type :string
|
# target_type :string
|
||||||
# target_id :bigint(8)
|
# target_id :bigint(8)
|
||||||
# recorded_changes :text default(""), not null
|
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
# human_identifier :string
|
||||||
|
# route_param :string
|
||||||
|
# permalink :string
|
||||||
#
|
#
|
||||||
|
|
||||||
class Admin::ActionLog < ApplicationRecord
|
class Admin::ActionLog < ApplicationRecord
|
||||||
serialize :recorded_changes
|
self.ignored_columns = %w(
|
||||||
|
recorded_changes
|
||||||
|
)
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :target, polymorphic: true, optional: true
|
belongs_to :target, polymorphic: true, optional: true
|
||||||
|
|
||||||
default_scope -> { order('id desc') }
|
default_scope -> { order('id desc') }
|
||||||
|
|
||||||
|
before_validation :set_human_identifier
|
||||||
|
before_validation :set_route_param
|
||||||
|
before_validation :set_permalink
|
||||||
|
|
||||||
def action
|
def action
|
||||||
super.to_sym
|
super.to_sym
|
||||||
end
|
end
|
||||||
|
|
||||||
before_validation :set_changes
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_changes
|
def set_human_identifier
|
||||||
case action
|
self.human_identifier = target.to_log_human_identifier if target.respond_to?(:to_log_human_identifier)
|
||||||
when :destroy, :create
|
end
|
||||||
self.recorded_changes = target.attributes
|
|
||||||
when :update, :promote, :demote
|
def set_route_param
|
||||||
self.recorded_changes = target.previous_changes
|
self.route_param = target.to_log_route_param if target.respond_to?(:to_log_route_param)
|
||||||
when :change_email
|
end
|
||||||
self.recorded_changes = ActiveSupport::HashWithIndifferentAccess.new(
|
|
||||||
email: [target.email, nil],
|
def set_permalink
|
||||||
unconfirmed_email: [nil, target.unconfirmed_email]
|
self.permalink = target.to_log_permalink if target.respond_to?(:to_log_permalink)
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -12,6 +12,7 @@ class Admin::ActionLogFilter
|
|||||||
reject_appeal: { target_type: 'Appeal', action: 'reject' }.freeze,
|
reject_appeal: { target_type: 'Appeal', action: 'reject' }.freeze,
|
||||||
assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
|
assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
|
||||||
change_email_user: { target_type: 'User', action: 'change_email' }.freeze,
|
change_email_user: { target_type: 'User', action: 'change_email' }.freeze,
|
||||||
|
change_role_user: { target_type: 'User', action: 'change_role' }.freeze,
|
||||||
confirm_user: { target_type: 'User', action: 'confirm' }.freeze,
|
confirm_user: { target_type: 'User', action: 'confirm' }.freeze,
|
||||||
approve_user: { target_type: 'User', action: 'approve' }.freeze,
|
approve_user: { target_type: 'User', action: 'approve' }.freeze,
|
||||||
reject_user: { target_type: 'User', action: 'reject' }.freeze,
|
reject_user: { target_type: 'User', action: 'reject' }.freeze,
|
||||||
@ -21,16 +22,22 @@ class Admin::ActionLogFilter
|
|||||||
create_domain_allow: { target_type: 'DomainAllow', action: 'create' }.freeze,
|
create_domain_allow: { target_type: 'DomainAllow', action: 'create' }.freeze,
|
||||||
create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze,
|
create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze,
|
||||||
create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze,
|
create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze,
|
||||||
|
create_ip_block: { target_type: 'IpBlock', action: 'create' }.freeze,
|
||||||
create_unavailable_domain: { target_type: 'UnavailableDomain', action: 'create' }.freeze,
|
create_unavailable_domain: { target_type: 'UnavailableDomain', action: 'create' }.freeze,
|
||||||
|
create_user_role: { target_type: 'UserRole', action: 'create' }.freeze,
|
||||||
|
create_canonical_email_block: { target_type: 'CanonicalEmailBlock', action: 'create' }.freeze,
|
||||||
demote_user: { target_type: 'User', action: 'demote' }.freeze,
|
demote_user: { target_type: 'User', action: 'demote' }.freeze,
|
||||||
destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze,
|
destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze,
|
||||||
destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze,
|
destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze,
|
||||||
destroy_domain_allow: { target_type: 'DomainAllow', action: 'destroy' }.freeze,
|
destroy_domain_allow: { target_type: 'DomainAllow', action: 'destroy' }.freeze,
|
||||||
destroy_domain_block: { target_type: 'DomainBlock', action: 'destroy' }.freeze,
|
destroy_domain_block: { target_type: 'DomainBlock', action: 'destroy' }.freeze,
|
||||||
|
destroy_ip_block: { target_type: 'IpBlock', action: 'destroy' }.freeze,
|
||||||
destroy_email_domain_block: { target_type: 'EmailDomainBlock', action: 'destroy' }.freeze,
|
destroy_email_domain_block: { target_type: 'EmailDomainBlock', action: 'destroy' }.freeze,
|
||||||
destroy_instance: { target_type: 'Instance', action: 'destroy' }.freeze,
|
destroy_instance: { target_type: 'Instance', action: 'destroy' }.freeze,
|
||||||
destroy_unavailable_domain: { target_type: 'UnavailableDomain', action: 'destroy' }.freeze,
|
destroy_unavailable_domain: { target_type: 'UnavailableDomain', action: 'destroy' }.freeze,
|
||||||
destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
|
destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
|
||||||
|
destroy_user_role: { target_type: 'UserRole', action: 'destroy' }.freeze,
|
||||||
|
destroy_canonical_email_block: { target_type: 'CanonicalEmailBlock', action: 'destroy' }.freeze,
|
||||||
disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze,
|
disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze,
|
||||||
disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,
|
disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,
|
||||||
disable_user: { target_type: 'User', action: 'disable' }.freeze,
|
disable_user: { target_type: 'User', action: 'disable' }.freeze,
|
||||||
@ -52,6 +59,8 @@ class Admin::ActionLogFilter
|
|||||||
update_announcement: { target_type: 'Announcement', action: 'update' }.freeze,
|
update_announcement: { target_type: 'Announcement', action: 'update' }.freeze,
|
||||||
update_custom_emoji: { target_type: 'CustomEmoji', action: 'update' }.freeze,
|
update_custom_emoji: { target_type: 'CustomEmoji', action: 'update' }.freeze,
|
||||||
update_status: { target_type: 'Status', action: 'update' }.freeze,
|
update_status: { target_type: 'Status', action: 'update' }.freeze,
|
||||||
|
update_user_role: { target_type: 'UserRole', action: 'update' }.freeze,
|
||||||
|
update_ip_block: { target_type: 'IpBlock', action: 'update' }.freeze,
|
||||||
unblock_email_account: { target_type: 'Account', action: 'unblock_email' }.freeze,
|
unblock_email_account: { target_type: 'Account', action: 'unblock_email' }.freeze,
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
@ -34,6 +34,10 @@ class Announcement < ApplicationRecord
|
|||||||
before_validation :set_all_day
|
before_validation :set_all_day
|
||||||
before_validation :set_published, on: :create
|
before_validation :set_published, on: :create
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
text
|
||||||
|
end
|
||||||
|
|
||||||
def publish!
|
def publish!
|
||||||
update!(published: true, published_at: Time.now.utc, scheduled_at: nil)
|
update!(published: true, published_at: Time.now.utc, scheduled_at: nil)
|
||||||
end
|
end
|
||||||
|
@ -52,6 +52,14 @@ class Appeal < ApplicationRecord
|
|||||||
update!(rejected_at: Time.now.utc, rejected_by_account: current_account)
|
update!(rejected_at: Time.now.utc, rejected_by_account: current_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
account.acct
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_log_route_param
|
||||||
|
account_warning_id
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def validate_time_frame
|
def validate_time_frame
|
||||||
|
@ -5,27 +5,30 @@
|
|||||||
#
|
#
|
||||||
# id :bigint(8) not null, primary key
|
# id :bigint(8) not null, primary key
|
||||||
# canonical_email_hash :string default(""), not null
|
# canonical_email_hash :string default(""), not null
|
||||||
# reference_account_id :bigint(8) not null
|
# reference_account_id :bigint(8)
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class CanonicalEmailBlock < ApplicationRecord
|
class CanonicalEmailBlock < ApplicationRecord
|
||||||
include EmailHelper
|
include EmailHelper
|
||||||
|
include Paginable
|
||||||
|
|
||||||
belongs_to :reference_account, class_name: 'Account'
|
belongs_to :reference_account, class_name: 'Account', optional: true
|
||||||
|
|
||||||
validates :canonical_email_hash, presence: true, uniqueness: true
|
validates :canonical_email_hash, presence: true, uniqueness: true
|
||||||
|
|
||||||
|
scope :matching_email, ->(email) { where(canonical_email_hash: email_to_canonical_email_hash(email)) }
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
canonical_email_hash
|
||||||
|
end
|
||||||
|
|
||||||
def email=(email)
|
def email=(email)
|
||||||
self.canonical_email_hash = email_to_canonical_email_hash(email)
|
self.canonical_email_hash = email_to_canonical_email_hash(email)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.block?(email)
|
def self.block?(email)
|
||||||
where(canonical_email_hash: email_to_canonical_email_hash(email)).exists?
|
matching_email(email).exists?
|
||||||
end
|
|
||||||
|
|
||||||
def self.find_blocks(email)
|
|
||||||
where(canonical_email_hash: email_to_canonical_email_hash(email))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -49,7 +49,7 @@ class CustomEmoji < ApplicationRecord
|
|||||||
scope :local, -> { where(domain: nil) }
|
scope :local, -> { where(domain: nil) }
|
||||||
scope :remote, -> { where.not(domain: nil) }
|
scope :remote, -> { where.not(domain: nil) }
|
||||||
scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) }
|
scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) }
|
||||||
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
|
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches("%.#{domain}"))) }
|
||||||
scope :listed, -> { local.where(disabled: false).where(visible_in_picker: true) }
|
scope :listed, -> { local.where(disabled: false).where(visible_in_picker: true) }
|
||||||
|
|
||||||
remotable_attachment :image, LIMIT
|
remotable_attachment :image, LIMIT
|
||||||
@ -70,6 +70,10 @@ class CustomEmoji < ApplicationRecord
|
|||||||
copy.tap(&:save!)
|
copy.tap(&:save!)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
shortcode
|
||||||
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def from_text(text, domain = nil)
|
def from_text(text, domain = nil)
|
||||||
return [] if text.blank?
|
return [] if text.blank?
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# id :bigint(8) not null, primary key
|
# id :bigint(8) not null, primary key
|
||||||
# custom_filter_id :bigint(8) not null
|
# custom_filter_id :bigint(8) not null
|
||||||
# status_id :bigint(8) default(""), not null
|
# status_id :bigint(8) not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
#
|
#
|
||||||
|
@ -19,6 +19,10 @@ class DomainAllow < ApplicationRecord
|
|||||||
|
|
||||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
domain
|
||||||
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def allowed?(domain)
|
def allowed?(domain)
|
||||||
!rule_for(domain).nil?
|
!rule_for(domain).nil?
|
||||||
|
@ -31,6 +31,10 @@ class DomainBlock < ApplicationRecord
|
|||||||
scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) }
|
scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) }
|
||||||
scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain')) }
|
scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain')) }
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
domain
|
||||||
|
end
|
||||||
|
|
||||||
def policies
|
def policies
|
||||||
if suspend?
|
if suspend?
|
||||||
[:suspend]
|
[:suspend]
|
||||||
|
@ -17,6 +17,7 @@ class EmailDomainBlock < ApplicationRecord
|
|||||||
)
|
)
|
||||||
|
|
||||||
include DomainNormalizable
|
include DomainNormalizable
|
||||||
|
include Paginable
|
||||||
|
|
||||||
belongs_to :parent, class_name: 'EmailDomainBlock', optional: true
|
belongs_to :parent, class_name: 'EmailDomainBlock', optional: true
|
||||||
has_many :children, class_name: 'EmailDomainBlock', foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
|
has_many :children, class_name: 'EmailDomainBlock', foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
|
||||||
@ -26,6 +27,10 @@ class EmailDomainBlock < ApplicationRecord
|
|||||||
# Used for adding multiple blocks at once
|
# Used for adding multiple blocks at once
|
||||||
attr_accessor :other_domains
|
attr_accessor :other_domains
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
domain
|
||||||
|
end
|
||||||
|
|
||||||
def history
|
def history
|
||||||
@history ||= Trends::History.new('email_domain_blocks', id)
|
@history ||= Trends::History.new('email_domain_blocks', id)
|
||||||
end
|
end
|
||||||
|
@ -6,7 +6,8 @@ class Form::AccountBatch
|
|||||||
include AccountableConcern
|
include AccountableConcern
|
||||||
include Payloadable
|
include Payloadable
|
||||||
|
|
||||||
attr_accessor :account_ids, :action, :current_account
|
attr_accessor :account_ids, :action, :current_account,
|
||||||
|
:select_all_matching, :query
|
||||||
|
|
||||||
def save
|
def save
|
||||||
case action
|
case action
|
||||||
@ -60,8 +61,12 @@ class Form::AccountBatch
|
|||||||
end
|
end
|
||||||
|
|
||||||
def accounts
|
def accounts
|
||||||
|
if select_all_matching?
|
||||||
|
query
|
||||||
|
else
|
||||||
Account.where(id: account_ids)
|
Account.where(id: account_ids)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def approve!
|
def approve!
|
||||||
accounts.includes(:user).find_each do |account|
|
accounts.includes(:user).find_each do |account|
|
||||||
@ -101,7 +106,7 @@ class Form::AccountBatch
|
|||||||
|
|
||||||
def reject_account(account)
|
def reject_account(account)
|
||||||
authorize(account.user, :reject?)
|
authorize(account.user, :reject?)
|
||||||
log_action(:reject, account.user, username: account.username)
|
log_action(:reject, account.user)
|
||||||
account.suspend!(origin: :local)
|
account.suspend!(origin: :local)
|
||||||
AccountDeletionWorker.perform_async(account.id, { 'reserve_username' => false })
|
AccountDeletionWorker.perform_async(account.id, { 'reserve_username' => false })
|
||||||
end
|
end
|
||||||
@ -118,4 +123,8 @@ class Form::AccountBatch
|
|||||||
log_action(:approve, account.user)
|
log_action(:approve, account.user)
|
||||||
account.user.approve!
|
account.user.approve!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def select_all_matching?
|
||||||
|
select_all_matching == '1'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -48,6 +48,8 @@ class Instance < ApplicationRecord
|
|||||||
domain
|
domain
|
||||||
end
|
end
|
||||||
|
|
||||||
|
alias to_log_human_identifier to_param
|
||||||
|
|
||||||
delegate :exhausted_deliveries_days, to: :delivery_failure_tracker
|
delegate :exhausted_deliveries_days, to: :delivery_failure_tracker
|
||||||
|
|
||||||
def availability_over_days(num_days, end_date = Time.now.utc.to_date)
|
def availability_over_days(num_days, end_date = Time.now.utc.to_date)
|
||||||
|
@ -16,6 +16,7 @@ class IpBlock < ApplicationRecord
|
|||||||
CACHE_KEY = 'blocked_ips'
|
CACHE_KEY = 'blocked_ips'
|
||||||
|
|
||||||
include Expireable
|
include Expireable
|
||||||
|
include Paginable
|
||||||
|
|
||||||
enum severity: {
|
enum severity: {
|
||||||
sign_up_requires_approval: 5000,
|
sign_up_requires_approval: 5000,
|
||||||
@ -27,6 +28,10 @@ class IpBlock < ApplicationRecord
|
|||||||
|
|
||||||
after_commit :reset_cache
|
after_commit :reset_cache
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
"#{ip}/#{ip.prefix}"
|
||||||
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def blocked?(remote_ip)
|
def blocked?(remote_ip)
|
||||||
blocked_ips_map = Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(IpBlock.where(severity: :no_access).pluck(:ip)) }
|
blocked_ips_map = Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(IpBlock.where(severity: :no_access).pluck(:ip)) }
|
||||||
|
@ -115,6 +115,10 @@ class Report < ApplicationRecord
|
|||||||
Report.where.not(id: id).where(target_account_id: target_account_id).unresolved.exists?
|
Report.where.not(id: id).where(target_account_id: target_account_id).unresolved.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
id
|
||||||
|
end
|
||||||
|
|
||||||
def history
|
def history
|
||||||
subquery = [
|
subquery = [
|
||||||
Admin::ActionLog.where(
|
Admin::ActionLog.where(
|
||||||
@ -136,6 +140,8 @@ class Report < ApplicationRecord
|
|||||||
Admin::ActionLog.from(Arel::Nodes::As.new(subquery, Admin::ActionLog.arel_table))
|
Admin::ActionLog.from(Arel::Nodes::As.new(subquery, Admin::ActionLog.arel_table))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def set_uri
|
def set_uri
|
||||||
self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? && account.local?
|
self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? && account.local?
|
||||||
end
|
end
|
||||||
|
@ -171,6 +171,14 @@ class Status < ApplicationRecord
|
|||||||
].compact.join("\n\n")
|
].compact.join("\n\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
account.acct
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_log_permalink
|
||||||
|
ActivityPub::TagManager.instance.uri_for(self)
|
||||||
|
end
|
||||||
|
|
||||||
def reply?
|
def reply?
|
||||||
!in_reply_to_id.nil? || attributes['reply']
|
!in_reply_to_id.nil? || attributes['reply']
|
||||||
end
|
end
|
||||||
|
@ -16,6 +16,10 @@ class UnavailableDomain < ApplicationRecord
|
|||||||
|
|
||||||
after_commit :reset_cache!
|
after_commit :reset_cache!
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
domain
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def reset_cache!
|
def reset_cache!
|
||||||
|
@ -181,6 +181,14 @@ class User < ApplicationRecord
|
|||||||
update!(disabled: false)
|
update!(disabled: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
account.acct
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_log_route_param
|
||||||
|
account_id
|
||||||
|
end
|
||||||
|
|
||||||
def confirm
|
def confirm
|
||||||
new_user = !confirmed?
|
new_user = !confirmed?
|
||||||
self.approved = true if open_registrations? && !sign_up_from_ip_requires_approval?
|
self.approved = true if open_registrations? && !sign_up_from_ip_requires_approval?
|
||||||
@ -281,10 +289,6 @@ class User < ApplicationRecord
|
|||||||
settings.default_privacy || (account.locked? ? 'private' : 'public')
|
settings.default_privacy || (account.locked? ? 'private' : 'public')
|
||||||
end
|
end
|
||||||
|
|
||||||
def allows_digest_emails?
|
|
||||||
settings.notification_emails['digest']
|
|
||||||
end
|
|
||||||
|
|
||||||
def allows_report_emails?
|
def allows_report_emails?
|
||||||
settings.notification_emails['report']
|
settings.notification_emails['report']
|
||||||
end
|
end
|
||||||
|
@ -155,6 +155,10 @@ class UserRole < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
name
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def in_permissions?(privilege)
|
def in_permissions?(privilege)
|
||||||
|
23
app/policies/canonical_email_block_policy.rb
Normal file
23
app/policies/canonical_email_block_policy.rb
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CanonicalEmailBlockPolicy < ApplicationPolicy
|
||||||
|
def index?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
end
|
@ -9,6 +9,10 @@ class IpBlockPolicy < ApplicationPolicy
|
|||||||
role.can?(:manage_blocks)
|
role.can?(:manage_blocks)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
role.can?(:manage_blocks)
|
||||||
|
end
|
||||||
|
|
||||||
def destroy?
|
def destroy?
|
||||||
role.can?(:manage_blocks)
|
role.can?(:manage_blocks)
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::Admin::CanonicalEmailBlockSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :canonical_email_hash
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id.to_s
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::Admin::EmailDomainBlockSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :domain, :created_at, :history
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id.to_s
|
||||||
|
end
|
||||||
|
end
|
14
app/serializers/rest/admin/ip_block_serializer.rb
Normal file
14
app/serializers/rest/admin/ip_block_serializer.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::Admin::IpBlockSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :ip, :severity, :comment,
|
||||||
|
:created_at, :expires_at
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def ip
|
||||||
|
"#{object.ip}/#{object.ip.prefix}"
|
||||||
|
end
|
||||||
|
end
|
@ -10,24 +10,18 @@ class ClearDomainMediaService < BaseService
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def invalidate_association_caches!
|
def invalidate_association_caches!(status_ids)
|
||||||
# Normally, associated models of a status are immutable (except for accounts)
|
# Normally, associated models of a status are immutable (except for accounts)
|
||||||
# so they are aggressively cached. After updating the media attachments to no
|
# so they are aggressively cached. After updating the media attachments to no
|
||||||
# longer point to a local file, we need to clear the cache to make those
|
# longer point to a local file, we need to clear the cache to make those
|
||||||
# changes appear in the API and UI
|
# changes appear in the API and UI
|
||||||
@affected_status_ids.each { |id| Rails.cache.delete_matched("statuses/#{id}-*") }
|
Rails.cache.delete_multi(status_ids.map { |id| "statuses/#{id}" })
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_media!
|
def clear_media!
|
||||||
@affected_status_ids = []
|
|
||||||
|
|
||||||
begin
|
|
||||||
clear_account_images!
|
clear_account_images!
|
||||||
clear_account_attachments!
|
clear_account_attachments!
|
||||||
clear_emojos!
|
clear_emojos!
|
||||||
ensure
|
|
||||||
invalidate_association_caches!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_account_images!
|
def clear_account_images!
|
||||||
@ -39,13 +33,19 @@ class ClearDomainMediaService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def clear_account_attachments!
|
def clear_account_attachments!
|
||||||
media_from_blocked_domain.reorder(nil).find_each do |attachment|
|
media_from_blocked_domain.reorder(nil).find_in_batches do |attachments|
|
||||||
@affected_status_ids << attachment.status_id if attachment.status_id.present?
|
affected_status_ids = []
|
||||||
|
|
||||||
|
attachments.each do |attachment|
|
||||||
|
affected_status_ids << attachment.status_id if attachment.status_id.present?
|
||||||
|
|
||||||
attachment.file.destroy if attachment.file&.exists?
|
attachment.file.destroy if attachment.file&.exists?
|
||||||
attachment.type = :unknown
|
attachment.type = :unknown
|
||||||
attachment.save
|
attachment.save
|
||||||
end
|
end
|
||||||
|
|
||||||
|
invalidate_association_caches!(affected_status_ids) unless affected_status_ids.empty?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_emojos!
|
def clear_emojos!
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
|
|
||||||
= form_for(@form, url: batch_admin_accounts_path) do |f|
|
= form_for(@form, url: batch_admin_accounts_path) do |f|
|
||||||
= hidden_field_tag :page, params[:page] || 1
|
= hidden_field_tag :page, params[:page] || 1
|
||||||
|
= hidden_field_tag :select_all_matching, '0'
|
||||||
|
|
||||||
- AccountFilter::KEYS.each do |key|
|
- AccountFilter::KEYS.each do |key|
|
||||||
= hidden_field_tag key, params[key] if params[key].present?
|
= hidden_field_tag key, params[key] if params[key].present?
|
||||||
@ -49,6 +50,14 @@
|
|||||||
= f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
= f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||||
|
|
||||||
= f.button safe_join([fa_icon('lock'), t('admin.accounts.perform_full_suspension')]), name: :suspend, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
= f.button safe_join([fa_icon('lock'), t('admin.accounts.perform_full_suspension')]), name: :suspend, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||||
|
- if @accounts.total_count > @accounts.size
|
||||||
|
.batch-table__select-all
|
||||||
|
.not-selected.active
|
||||||
|
%span= t('generic.all_items_on_page_selected_html', count: @accounts.size)
|
||||||
|
%button{ type: 'button' }= t('generic.select_all_matching_items', count: @accounts.total_count)
|
||||||
|
.selected
|
||||||
|
%span= t('generic.all_matching_items_selected_html', count: @accounts.total_count)
|
||||||
|
%button{ type: 'button' }= t('generic.deselect')
|
||||||
.batch-table__body
|
.batch-table__body
|
||||||
- if @accounts.empty?
|
- if @accounts.empty?
|
||||||
= nothing_here 'nothing-here--under-tabs'
|
= nothing_here 'nothing-here--under-tabs'
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
%link{ rel: 'manifest', href: manifest_path(format: :json) }/
|
%link{ rel: 'manifest', href: manifest_path(format: :json) }/
|
||||||
%meta{ name: 'theme-color', content: '#6364FF' }/
|
%meta{ name: 'theme-color', content: '#6364FF' }/
|
||||||
%meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/
|
%meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/
|
||||||
|
%meta{ name: 'apple-itunes-app', content: 'app-id=1571998974' }/
|
||||||
|
|
||||||
%title= content_for?(:page_title) ? safe_join([yield(:page_title).chomp.html_safe, title], ' - ') : title
|
%title= content_for?(:page_title) ? safe_join([yield(:page_title).chomp.html_safe, title], ' - ') : title
|
||||||
|
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
|
||||||
%tbody
|
|
||||||
%tr
|
|
||||||
%td.email-body
|
|
||||||
.email-container
|
|
||||||
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
|
||||||
%tbody
|
|
||||||
%tr
|
|
||||||
%td.content-cell.darker.hero-with-button
|
|
||||||
.email-row
|
|
||||||
.col-6
|
|
||||||
%table.column{ cellspacing: 0, cellpadding: 0 }
|
|
||||||
%tbody
|
|
||||||
%tr
|
|
||||||
%td.column-cell.text-center.padded
|
|
||||||
%h1= t 'notification_mailer.digest.title'
|
|
||||||
%p.lead= t('notification_mailer.digest.body', since: l((@me.user_current_sign_in_at || @since).to_date, format: :short), instance: site_hostname)
|
|
||||||
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
|
||||||
%tbody
|
|
||||||
%tr
|
|
||||||
%td.button-primary
|
|
||||||
= link_to web_url do
|
|
||||||
%span= t 'notification_mailer.digest.action'
|
|
||||||
|
|
||||||
- @notifications.each_with_index do |n, i|
|
|
||||||
= render 'status', status: n.target_status, i: i
|
|
||||||
|
|
||||||
- unless @follows_since.zero?
|
|
||||||
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
|
||||||
%tbody
|
|
||||||
%tr
|
|
||||||
%td.email-body
|
|
||||||
.email-container
|
|
||||||
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
|
||||||
%tbody
|
|
||||||
%tr
|
|
||||||
%td.content-cell.content-start.border-top
|
|
||||||
.email-row
|
|
||||||
.col-6
|
|
||||||
%table.column{ cellspacing: 0, cellpadding: 0 }
|
|
||||||
%tbody
|
|
||||||
%tr
|
|
||||||
%td.column-cell.text-center
|
|
||||||
%p= t('notification_mailer.digest.new_followers_summary', count: @follows_since)
|
|
@ -1,15 +0,0 @@
|
|||||||
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
|
|
||||||
|
|
||||||
<%= raw t('notification_mailer.digest.body', since: l(@me.user_current_sign_in_at || @since), instance: root_url) %>
|
|
||||||
<% @notifications.each do |notification| %>
|
|
||||||
|
|
||||||
* <%= raw t('notification_mailer.digest.mention', name: notification.from_account.pretty_acct) %>
|
|
||||||
|
|
||||||
<%= raw extract_status_plain_text(notification.target_status) %>
|
|
||||||
|
|
||||||
<%= raw t('application_mailer.view')%> <%= web_url("statuses/#{notification.target_status.id}") %>
|
|
||||||
<% end %>
|
|
||||||
<% if @follows_since > 0 %>
|
|
||||||
|
|
||||||
<%= raw t('notification_mailer.digest.new_followers_summary', count: @follows_since) %>
|
|
||||||
<% end %>
|
|
@ -28,10 +28,6 @@
|
|||||||
.fields-group
|
.fields-group
|
||||||
= f.input :setting_always_send_emails, as: :boolean, wrapper: :with_label
|
= f.input :setting_always_send_emails, as: :boolean, wrapper: :with_label
|
||||||
|
|
||||||
.fields-group
|
|
||||||
= f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
|
|
||||||
= ff.input :digest, as: :boolean, wrapper: :with_label
|
|
||||||
|
|
||||||
%h4= t 'notifications.other_settings'
|
%h4= t 'notifications.other_settings'
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
|
@ -10,9 +10,6 @@
|
|||||||
.fields-group
|
.fields-group
|
||||||
= f.input :setting_noindex, as: :boolean, wrapper: :with_label
|
= f.input :setting_noindex, as: :boolean, wrapper: :with_label
|
||||||
|
|
||||||
.fields-group
|
|
||||||
= f.input :setting_aggregate_reblogs, as: :boolean, wrapper: :with_label, recommended: true
|
|
||||||
|
|
||||||
- unless Setting.hide_followers_count
|
- unless Setting.hide_followers_count
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :setting_hide_followers_count, as: :boolean, wrapper: :with_label
|
= f.input :setting_hide_followers_count, as: :boolean, wrapper: :with_label
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class DigestMailerWorker
|
|
||||||
include Sidekiq::Worker
|
|
||||||
|
|
||||||
sidekiq_options queue: 'mailers'
|
|
||||||
|
|
||||||
attr_reader :user
|
|
||||||
|
|
||||||
def perform(user_id)
|
|
||||||
@user = User.find(user_id)
|
|
||||||
deliver_digest if @user.allows_digest_emails?
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def deliver_digest
|
|
||||||
NotificationMailer.digest(user.account).deliver_now!
|
|
||||||
user.touch(:last_emailed_at)
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,25 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Scheduler::EmailScheduler
|
|
||||||
include Sidekiq::Worker
|
|
||||||
|
|
||||||
sidekiq_options retry: 0
|
|
||||||
|
|
||||||
FREQUENCY = 7.days.freeze
|
|
||||||
SIGN_IN_OFFSET = 1.day.freeze
|
|
||||||
|
|
||||||
def perform
|
|
||||||
eligible_users.reorder(nil).find_each do |user|
|
|
||||||
next unless user.allows_digest_emails?
|
|
||||||
DigestMailerWorker.perform_async(user.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def eligible_users
|
|
||||||
User.emailable
|
|
||||||
.where('current_sign_in_at < ?', (FREQUENCY + SIGN_IN_OFFSET).ago)
|
|
||||||
.where('last_emailed_at IS NULL OR last_emailed_at < ?', FREQUENCY.ago)
|
|
||||||
end
|
|
||||||
end
|
|
@ -235,17 +235,21 @@ en:
|
|||||||
approve_user: Approve User
|
approve_user: Approve User
|
||||||
assigned_to_self_report: Assign Report
|
assigned_to_self_report: Assign Report
|
||||||
change_email_user: Change E-mail for User
|
change_email_user: Change E-mail for User
|
||||||
|
change_role_user: Change Role of User
|
||||||
confirm_user: Confirm User
|
confirm_user: Confirm User
|
||||||
create_account_warning: Create Warning
|
create_account_warning: Create Warning
|
||||||
create_announcement: Create Announcement
|
create_announcement: Create Announcement
|
||||||
|
create_canonical_email_block: Create E-mail Block
|
||||||
create_custom_emoji: Create Custom Emoji
|
create_custom_emoji: Create Custom Emoji
|
||||||
create_domain_allow: Create Domain Allow
|
create_domain_allow: Create Domain Allow
|
||||||
create_domain_block: Create Domain Block
|
create_domain_block: Create Domain Block
|
||||||
create_email_domain_block: Create E-mail Domain Block
|
create_email_domain_block: Create E-mail Domain Block
|
||||||
create_ip_block: Create IP rule
|
create_ip_block: Create IP rule
|
||||||
create_unavailable_domain: Create Unavailable Domain
|
create_unavailable_domain: Create Unavailable Domain
|
||||||
|
create_user_role: Create Role
|
||||||
demote_user: Demote User
|
demote_user: Demote User
|
||||||
destroy_announcement: Delete Announcement
|
destroy_announcement: Delete Announcement
|
||||||
|
destroy_canonical_email_block: Delete E-mail Block
|
||||||
destroy_custom_emoji: Delete Custom Emoji
|
destroy_custom_emoji: Delete Custom Emoji
|
||||||
destroy_domain_allow: Delete Domain Allow
|
destroy_domain_allow: Delete Domain Allow
|
||||||
destroy_domain_block: Delete Domain Block
|
destroy_domain_block: Delete Domain Block
|
||||||
@ -254,6 +258,7 @@ en:
|
|||||||
destroy_ip_block: Delete IP rule
|
destroy_ip_block: Delete IP rule
|
||||||
destroy_status: Delete Post
|
destroy_status: Delete Post
|
||||||
destroy_unavailable_domain: Delete Unavailable Domain
|
destroy_unavailable_domain: Delete Unavailable Domain
|
||||||
|
destroy_user_role: Destroy Role
|
||||||
disable_2fa_user: Disable 2FA
|
disable_2fa_user: Disable 2FA
|
||||||
disable_custom_emoji: Disable Custom Emoji
|
disable_custom_emoji: Disable Custom Emoji
|
||||||
disable_sign_in_token_auth_user: Disable E-mail Token Authentication for User
|
disable_sign_in_token_auth_user: Disable E-mail Token Authentication for User
|
||||||
@ -280,24 +285,30 @@ en:
|
|||||||
update_announcement: Update Announcement
|
update_announcement: Update Announcement
|
||||||
update_custom_emoji: Update Custom Emoji
|
update_custom_emoji: Update Custom Emoji
|
||||||
update_domain_block: Update Domain Block
|
update_domain_block: Update Domain Block
|
||||||
|
update_ip_block: Update IP rule
|
||||||
update_status: Update Post
|
update_status: Update Post
|
||||||
|
update_user_role: Update Role
|
||||||
actions:
|
actions:
|
||||||
approve_appeal_html: "%{name} approved moderation decision appeal from %{target}"
|
approve_appeal_html: "%{name} approved moderation decision appeal from %{target}"
|
||||||
approve_user_html: "%{name} approved sign-up from %{target}"
|
approve_user_html: "%{name} approved sign-up from %{target}"
|
||||||
assigned_to_self_report_html: "%{name} assigned report %{target} to themselves"
|
assigned_to_self_report_html: "%{name} assigned report %{target} to themselves"
|
||||||
change_email_user_html: "%{name} changed the e-mail address of user %{target}"
|
change_email_user_html: "%{name} changed the e-mail address of user %{target}"
|
||||||
|
change_role_user_html: "%{name} changed role of %{target}"
|
||||||
confirm_user_html: "%{name} confirmed e-mail address of user %{target}"
|
confirm_user_html: "%{name} confirmed e-mail address of user %{target}"
|
||||||
create_account_warning_html: "%{name} sent a warning to %{target}"
|
create_account_warning_html: "%{name} sent a warning to %{target}"
|
||||||
create_announcement_html: "%{name} created new announcement %{target}"
|
create_announcement_html: "%{name} created new announcement %{target}"
|
||||||
|
create_canonical_email_block_html: "%{name} blocked e-mail with the hash %{target}"
|
||||||
create_custom_emoji_html: "%{name} uploaded new emoji %{target}"
|
create_custom_emoji_html: "%{name} uploaded new emoji %{target}"
|
||||||
create_domain_allow_html: "%{name} allowed federation with domain %{target}"
|
create_domain_allow_html: "%{name} allowed federation with domain %{target}"
|
||||||
create_domain_block_html: "%{name} blocked domain %{target}"
|
create_domain_block_html: "%{name} blocked domain %{target}"
|
||||||
create_email_domain_block_html: "%{name} blocked e-mail domain %{target}"
|
create_email_domain_block_html: "%{name} blocked e-mail domain %{target}"
|
||||||
create_ip_block_html: "%{name} created rule for IP %{target}"
|
create_ip_block_html: "%{name} created rule for IP %{target}"
|
||||||
create_unavailable_domain_html: "%{name} stopped delivery to domain %{target}"
|
create_unavailable_domain_html: "%{name} stopped delivery to domain %{target}"
|
||||||
|
create_user_role_html: "%{name} created %{target} role"
|
||||||
demote_user_html: "%{name} demoted user %{target}"
|
demote_user_html: "%{name} demoted user %{target}"
|
||||||
destroy_announcement_html: "%{name} deleted announcement %{target}"
|
destroy_announcement_html: "%{name} deleted announcement %{target}"
|
||||||
destroy_custom_emoji_html: "%{name} destroyed emoji %{target}"
|
destroy_canonical_email_block_html: "%{name} unblocked e-mail with the hash %{target}"
|
||||||
|
destroy_custom_emoji_html: "%{name} deleted emoji %{target}"
|
||||||
destroy_domain_allow_html: "%{name} disallowed federation with domain %{target}"
|
destroy_domain_allow_html: "%{name} disallowed federation with domain %{target}"
|
||||||
destroy_domain_block_html: "%{name} unblocked domain %{target}"
|
destroy_domain_block_html: "%{name} unblocked domain %{target}"
|
||||||
destroy_email_domain_block_html: "%{name} unblocked e-mail domain %{target}"
|
destroy_email_domain_block_html: "%{name} unblocked e-mail domain %{target}"
|
||||||
@ -305,6 +316,7 @@ en:
|
|||||||
destroy_ip_block_html: "%{name} deleted rule for IP %{target}"
|
destroy_ip_block_html: "%{name} deleted rule for IP %{target}"
|
||||||
destroy_status_html: "%{name} removed post by %{target}"
|
destroy_status_html: "%{name} removed post by %{target}"
|
||||||
destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}"
|
destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}"
|
||||||
|
destroy_user_role_html: "%{name} deleted %{target} role"
|
||||||
disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}"
|
disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}"
|
||||||
disable_custom_emoji_html: "%{name} disabled emoji %{target}"
|
disable_custom_emoji_html: "%{name} disabled emoji %{target}"
|
||||||
disable_sign_in_token_auth_user_html: "%{name} disabled e-mail token authentication for %{target}"
|
disable_sign_in_token_auth_user_html: "%{name} disabled e-mail token authentication for %{target}"
|
||||||
@ -331,8 +343,9 @@ en:
|
|||||||
update_announcement_html: "%{name} updated announcement %{target}"
|
update_announcement_html: "%{name} updated announcement %{target}"
|
||||||
update_custom_emoji_html: "%{name} updated emoji %{target}"
|
update_custom_emoji_html: "%{name} updated emoji %{target}"
|
||||||
update_domain_block_html: "%{name} updated domain block for %{target}"
|
update_domain_block_html: "%{name} updated domain block for %{target}"
|
||||||
|
update_ip_block_html: "%{name} changed rule for IP %{target}"
|
||||||
update_status_html: "%{name} updated post by %{target}"
|
update_status_html: "%{name} updated post by %{target}"
|
||||||
deleted_status: "(deleted post)"
|
update_user_role_html: "%{name} changed %{target} role"
|
||||||
empty: No logs found.
|
empty: No logs found.
|
||||||
filter_by_action: Filter by action
|
filter_by_action: Filter by action
|
||||||
filter_by_user: Filter by user
|
filter_by_user: Filter by user
|
||||||
@ -1220,12 +1233,22 @@ en:
|
|||||||
trending_now: Trending now
|
trending_now: Trending now
|
||||||
generic:
|
generic:
|
||||||
all: All
|
all: All
|
||||||
|
all_items_on_page_selected_html:
|
||||||
|
one: "<strong>%{count}</strong> item on this page is selected."
|
||||||
|
other: All <strong>%{count}</strong> items on this page are selected.
|
||||||
|
all_matching_items_selected_html:
|
||||||
|
one: "<strong>%{count}</strong> item matching your search is selected."
|
||||||
|
other: All <strong>%{count}</strong> items matching your search are selected.
|
||||||
changes_saved_msg: Changes successfully saved!
|
changes_saved_msg: Changes successfully saved!
|
||||||
copy: Copy
|
copy: Copy
|
||||||
delete: Delete
|
delete: Delete
|
||||||
|
deselect: Deselect all
|
||||||
none: None
|
none: None
|
||||||
order_by: Order by
|
order_by: Order by
|
||||||
save_changes: Save changes
|
save_changes: Save changes
|
||||||
|
select_all_matching_items:
|
||||||
|
one: Select %{count} item matching your search.
|
||||||
|
other: Select all %{count} items matching your search.
|
||||||
today: today
|
today: today
|
||||||
validation_errors:
|
validation_errors:
|
||||||
one: Something isn't quite right yet! Please review the error below
|
one: Something isn't quite right yet! Please review the error below
|
||||||
@ -1334,17 +1357,6 @@ en:
|
|||||||
subject: "%{name} submitted a report"
|
subject: "%{name} submitted a report"
|
||||||
sign_up:
|
sign_up:
|
||||||
subject: "%{name} signed up"
|
subject: "%{name} signed up"
|
||||||
digest:
|
|
||||||
action: View all notifications
|
|
||||||
body: Here is a brief summary of the messages you missed since your last visit on %{since}
|
|
||||||
mention: "%{name} mentioned you in:"
|
|
||||||
new_followers_summary:
|
|
||||||
one: Also, you have acquired one new follower while being away! Yay!
|
|
||||||
other: Also, you have acquired %{count} new followers while being away! Amazing!
|
|
||||||
subject:
|
|
||||||
one: "1 new notification since your last visit 🐘"
|
|
||||||
other: "%{count} new notifications since your last visit 🐘"
|
|
||||||
title: In your absence...
|
|
||||||
favourite:
|
favourite:
|
||||||
body: 'Your post was favourited by %{name}:'
|
body: 'Your post was favourited by %{name}:'
|
||||||
subject: "%{name} favourited your post"
|
subject: "%{name} favourited your post"
|
||||||
|
@ -615,6 +615,8 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
resources :domain_allows, only: [:index, :show, :create, :destroy]
|
resources :domain_allows, only: [:index, :show, :create, :destroy]
|
||||||
resources :domain_blocks, only: [:index, :show, :update, :create, :destroy]
|
resources :domain_blocks, only: [:index, :show, :update, :create, :destroy]
|
||||||
|
resources :email_domain_blocks, only: [:index, :show, :create, :destroy]
|
||||||
|
resources :ip_blocks, only: [:index, :show, :update, :create, :destroy]
|
||||||
|
|
||||||
namespace :trends do
|
namespace :trends do
|
||||||
resources :tags, only: [:index]
|
resources :tags, only: [:index]
|
||||||
@ -625,6 +627,12 @@ Rails.application.routes.draw do
|
|||||||
post :measures, to: 'measures#create'
|
post :measures, to: 'measures#create'
|
||||||
post :dimensions, to: 'dimensions#create'
|
post :dimensions, to: 'dimensions#create'
|
||||||
post :retention, to: 'retention#create'
|
post :retention, to: 'retention#create'
|
||||||
|
|
||||||
|
resources :canonical_email_blocks, only: [:index, :create, :show, :destroy] do
|
||||||
|
collection do
|
||||||
|
post :test
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -49,10 +49,6 @@
|
|||||||
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
||||||
class: Scheduler::IpCleanupScheduler
|
class: Scheduler::IpCleanupScheduler
|
||||||
queue: scheduler
|
queue: scheduler
|
||||||
email_scheduler:
|
|
||||||
cron: '0 10 * * 2'
|
|
||||||
class: Scheduler::EmailScheduler
|
|
||||||
queue: scheduler
|
|
||||||
backup_cleanup_scheduler:
|
backup_cleanup_scheduler:
|
||||||
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
||||||
class: Scheduler::BackupCleanupScheduler
|
class: Scheduler::BackupCleanupScheduler
|
||||||
|
@ -1,29 +1,16 @@
|
|||||||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
||||||
|
|
||||||
const path = require('path');
|
const { createHash } = require('crypto');
|
||||||
const { URL } = require('url');
|
const { readFileSync } = require('fs');
|
||||||
|
const { resolve } = require('path');
|
||||||
const { merge } = require('webpack-merge');
|
const { merge } = require('webpack-merge');
|
||||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||||
const OfflinePlugin = require('offline-plugin');
|
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
const CompressionPlugin = require('compression-webpack-plugin');
|
const CompressionPlugin = require('compression-webpack-plugin');
|
||||||
const { output } = require('./configuration');
|
const { InjectManifest } = require('workbox-webpack-plugin');
|
||||||
const sharedConfig = require('./shared');
|
const sharedConfig = require('./shared');
|
||||||
|
|
||||||
let attachmentHost;
|
const root = resolve(__dirname, '..', '..');
|
||||||
|
|
||||||
if (process.env.S3_ENABLED === 'true') {
|
|
||||||
if (process.env.S3_ALIAS_HOST || process.env.S3_CLOUDFRONT_HOST) {
|
|
||||||
attachmentHost = process.env.S3_ALIAS_HOST || process.env.S3_CLOUDFRONT_HOST;
|
|
||||||
} else {
|
|
||||||
attachmentHost = process.env.S3_HOSTNAME || `s3-${process.env.S3_REGION || 'us-east-1'}.amazonaws.com`;
|
|
||||||
}
|
|
||||||
} else if (process.env.SWIFT_ENABLED === 'true') {
|
|
||||||
const { host } = new URL(process.env.SWIFT_OBJECT_URL);
|
|
||||||
attachmentHost = host;
|
|
||||||
} else {
|
|
||||||
attachmentHost = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = merge(sharedConfig, {
|
module.exports = merge(sharedConfig, {
|
||||||
mode: 'production',
|
mode: 'production',
|
||||||
@ -52,47 +39,28 @@ module.exports = merge(sharedConfig, {
|
|||||||
openAnalyzer: false,
|
openAnalyzer: false,
|
||||||
logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout
|
logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout
|
||||||
}),
|
}),
|
||||||
new OfflinePlugin({
|
new InjectManifest({
|
||||||
publicPath: output.publicPath, // sw.js must be served from the root to avoid scope issues
|
additionalManifestEntries: ['1f602.svg', 'sheet_13.png'].map((filename) => {
|
||||||
safeToUseOptionalCaches: true,
|
const path = resolve(root, 'public', 'emoji', filename);
|
||||||
caches: {
|
const body = readFileSync(path);
|
||||||
main: [':rest:'],
|
const md5 = createHash('md5');
|
||||||
additional: [':externals:'],
|
|
||||||
optional: [
|
md5.update(body);
|
||||||
'**/locale_*.js', // don't fetch every locale; the user only needs one
|
|
||||||
'**/*_polyfills-*.js', // the user may not need polyfills
|
return {
|
||||||
'**/*.woff2', // the user may have system-fonts enabled
|
revision: md5.digest('hex'),
|
||||||
// images/audio can be cached on-demand
|
url: `/emoji/${filename}`,
|
||||||
'**/*.png',
|
};
|
||||||
'**/*.jpg',
|
}),
|
||||||
'**/*.jpeg',
|
exclude: [
|
||||||
'**/*.svg',
|
/(?:base|extra)_polyfills-.*\.js$/,
|
||||||
'**/*.mp3',
|
/locale_.*\.js$/,
|
||||||
'**/*.ogg',
|
/mailer-.*\.(?:css|js)$/,
|
||||||
],
|
],
|
||||||
},
|
include: [/\.js$/, /\.css$/],
|
||||||
externals: [
|
maximumFileSizeToCacheInBytes: 2 * 1_024 * 1_024, // 2 MiB
|
||||||
'/emoji/1f602.svg', // used for emoji picker dropdown
|
swDest: resolve(root, 'public', 'packs', 'sw.js'),
|
||||||
'/emoji/sheet_10.png', // used in emoji-mart
|
swSrc: resolve(root, 'app', 'javascript', 'mastodon', 'service_worker', 'entry.js'),
|
||||||
],
|
|
||||||
excludes: [
|
|
||||||
'**/*.gz',
|
|
||||||
'**/*.map',
|
|
||||||
'stats.json',
|
|
||||||
'report.html',
|
|
||||||
// any browser that supports ServiceWorker will support woff2
|
|
||||||
'**/*.eot',
|
|
||||||
'**/*.ttf',
|
|
||||||
'**/*-webfont-*.svg',
|
|
||||||
'**/*.woff',
|
|
||||||
],
|
|
||||||
ServiceWorker: {
|
|
||||||
entry: `imports-loader?additionalCode=${encodeURIComponent(`var ATTACHMENT_HOST=${JSON.stringify(attachmentHost)};`)}!${encodeURI(path.join(__dirname, '../../app/javascript/mastodon/service_worker/entry.js'))}`,
|
|
||||||
cacheName: 'mastodon',
|
|
||||||
output: '../assets/sw.js',
|
|
||||||
publicPath: '/sw.js',
|
|
||||||
minify: true,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
class AddHumanIdentifierToAdminActionLogs < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :admin_action_logs, :human_identifier, :string
|
||||||
|
add_column :admin_action_logs, :route_param, :string
|
||||||
|
add_column :admin_action_logs, :permalink, :string
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,5 @@
|
|||||||
|
class ChangeCanonicalEmailBlocksNullable < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
safety_assured { change_column :canonical_email_blocks, :reference_account_id, :bigint, null: true, default: nil }
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class FixCustomFilterKeywordsIdSeq < ActiveRecord::Migration[6.1]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
# 20220613110711 manually inserts items with set `id` in the database, but
|
||||||
|
# we also need to bump the sequence number, otherwise
|
||||||
|
safety_assured do
|
||||||
|
execute <<-SQL.squish
|
||||||
|
BEGIN;
|
||||||
|
LOCK TABLE custom_filter_keywords IN EXCLUSIVE MODE;
|
||||||
|
SELECT setval('custom_filter_keywords_id_seq'::regclass, id) FROM custom_filter_keywords ORDER BY id DESC LIMIT 1;
|
||||||
|
COMMIT;
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down; end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoveRecordedChangesFromAdminActionLogs < ActiveRecord::Migration[5.2]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def change
|
||||||
|
safety_assured { remove_column :admin_action_logs, :recorded_changes, :text }
|
||||||
|
end
|
||||||
|
end
|
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2022_08_08_101323) do
|
ActiveRecord::Schema.define(version: 2022_08_27_195229) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@ -205,9 +205,11 @@ ActiveRecord::Schema.define(version: 2022_08_08_101323) do
|
|||||||
t.string "action", default: "", null: false
|
t.string "action", default: "", null: false
|
||||||
t.string "target_type"
|
t.string "target_type"
|
||||||
t.bigint "target_id"
|
t.bigint "target_id"
|
||||||
t.text "recorded_changes", default: "", null: false
|
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.string "human_identifier"
|
||||||
|
t.string "route_param"
|
||||||
|
t.string "permalink"
|
||||||
t.index ["account_id"], name: "index_admin_action_logs_on_account_id"
|
t.index ["account_id"], name: "index_admin_action_logs_on_account_id"
|
||||||
t.index ["target_type", "target_id"], name: "index_admin_action_logs_on_target_type_and_target_id"
|
t.index ["target_type", "target_id"], name: "index_admin_action_logs_on_target_type_and_target_id"
|
||||||
end
|
end
|
||||||
@ -294,7 +296,7 @@ ActiveRecord::Schema.define(version: 2022_08_08_101323) do
|
|||||||
|
|
||||||
create_table "canonical_email_blocks", force: :cascade do |t|
|
create_table "canonical_email_blocks", force: :cascade do |t|
|
||||||
t.string "canonical_email_hash", default: "", null: false
|
t.string "canonical_email_hash", default: "", null: false
|
||||||
t.bigint "reference_account_id", null: false
|
t.bigint "reference_account_id"
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
t.index ["canonical_email_hash"], name: "index_canonical_email_blocks_on_canonical_email_hash", unique: true
|
t.index ["canonical_email_hash"], name: "index_canonical_email_blocks_on_canonical_email_hash", unique: true
|
||||||
|
@ -15,7 +15,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
restart: always
|
restart: always
|
||||||
image: redis:6-alpine
|
image: redis:7-alpine
|
||||||
networks:
|
networks:
|
||||||
- internal_network
|
- internal_network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
@ -18,17 +18,15 @@ module Mastodon
|
|||||||
When suspending a local user, a hash of a "canonical" version of their e-mail
|
When suspending a local user, a hash of a "canonical" version of their e-mail
|
||||||
address is stored to prevent them from signing up again.
|
address is stored to prevent them from signing up again.
|
||||||
|
|
||||||
This command can be used to find whether a known email address is blocked,
|
This command can be used to find whether a known email address is blocked.
|
||||||
and if so, which account it was attached to.
|
|
||||||
LONG_DESC
|
LONG_DESC
|
||||||
def find(email)
|
def find(email)
|
||||||
accts = CanonicalEmailBlock.find_blocks(email).map(&:reference_account).map(&:acct).to_a
|
accts = CanonicalEmailBlock.matching_email(email)
|
||||||
|
|
||||||
if accts.empty?
|
if accts.empty?
|
||||||
say("#{email} is not blocked", :yellow)
|
say("#{email} is not blocked", :green)
|
||||||
else
|
else
|
||||||
accts.each do |acct|
|
say("#{email} is blocked", :red)
|
||||||
say(acct, :white)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -40,24 +38,13 @@ module Mastodon
|
|||||||
This command allows removing a canonical email block.
|
This command allows removing a canonical email block.
|
||||||
LONG_DESC
|
LONG_DESC
|
||||||
def remove(email)
|
def remove(email)
|
||||||
blocks = CanonicalEmailBlock.find_blocks(email)
|
blocks = CanonicalEmailBlock.matching_email(email)
|
||||||
|
|
||||||
if blocks.empty?
|
if blocks.empty?
|
||||||
say("#{email} is not blocked", :yellow)
|
say("#{email} is not blocked", :green)
|
||||||
else
|
else
|
||||||
blocks.destroy_all
|
blocks.destroy_all
|
||||||
say("Removed canonical email block for #{email}", :green)
|
say("Unblocked #{email}", :green)
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def color(processed, failed)
|
|
||||||
if !processed.zero? && failed.zero?
|
|
||||||
:green
|
|
||||||
elsif failed.zero?
|
|
||||||
:yellow
|
|
||||||
else
|
|
||||||
:red
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -84,7 +84,6 @@
|
|||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
"object-fit-images": "^3.2.3",
|
"object-fit-images": "^3.2.3",
|
||||||
"object.values": "^1.1.5",
|
"object.values": "^1.1.5",
|
||||||
"offline-plugin": "^5.0.7",
|
|
||||||
"path-complete-extname": "^1.0.0",
|
"path-complete-extname": "^1.0.0",
|
||||||
"pg": "^8.5.0",
|
"pg": "^8.5.0",
|
||||||
"postcss": "^8.4.16",
|
"postcss": "^8.4.16",
|
||||||
@ -138,6 +137,12 @@
|
|||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-merge": "^5.8.0",
|
"webpack-merge": "^5.8.0",
|
||||||
"wicg-inert": "^3.1.2",
|
"wicg-inert": "^3.1.2",
|
||||||
|
"workbox-expiration": "^6.5.3",
|
||||||
|
"workbox-precaching": "^6.5.3",
|
||||||
|
"workbox-routing": "^6.5.3",
|
||||||
|
"workbox-strategies": "^6.5.3",
|
||||||
|
"workbox-webpack-plugin": "^6.5.3",
|
||||||
|
"workbox-window": "^6.5.3",
|
||||||
"ws": "^8.8.1"
|
"ws": "^8.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1 +1 @@
|
|||||||
assets/sw.js
|
packs/sw.js
|
1
public/sw.js.map
Symbolic link
1
public/sw.js.map
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
packs/sw.js.map
|
@ -3,32 +3,4 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Admin::ActionLogsHelper, type: :helper do
|
RSpec.describe Admin::ActionLogsHelper, type: :helper do
|
||||||
klass = Class.new do
|
|
||||||
include ActionView::Helpers
|
|
||||||
include Admin::ActionLogsHelper
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:hoge) { klass.new }
|
|
||||||
|
|
||||||
describe '#log_target' do
|
|
||||||
after do
|
|
||||||
hoge.log_target(log)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'log.target' do
|
|
||||||
let(:log) { double(target: true) }
|
|
||||||
|
|
||||||
it 'calls linkable_log_target' do
|
|
||||||
expect(hoge).to receive(:linkable_log_target).with(log.target)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context '!log.target' do
|
|
||||||
let(:log) { double(target: false, target_type: :type, recorded_changes: :change) }
|
|
||||||
|
|
||||||
it 'calls log_target_from_history' do
|
|
||||||
expect(hoge).to receive(:log_target_from_history).with(log.target_type, log.recorded_changes)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -101,35 +101,4 @@ RSpec.describe NotificationMailer, type: :mailer do
|
|||||||
expect(mail.body.encoded).to match("bob has requested to follow you")
|
expect(mail.body.encoded).to match("bob has requested to follow you")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'digest' do
|
|
||||||
before do
|
|
||||||
mention = Fabricate(:mention, account: receiver.account, status: foreign_status)
|
|
||||||
Fabricate(:notification, account: receiver.account, activity: mention)
|
|
||||||
sender.follow!(receiver.account)
|
|
||||||
end
|
|
||||||
|
|
||||||
context do
|
|
||||||
let!(:mail) { NotificationMailer.digest(receiver.account, since: 5.days.ago) }
|
|
||||||
|
|
||||||
include_examples 'localized subject', 'notification_mailer.digest.subject', count: 1, name: 'bob'
|
|
||||||
|
|
||||||
it 'renders the headers' do
|
|
||||||
expect(mail.subject).to match('notification since your last')
|
|
||||||
expect(mail.to).to eq([receiver.email])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders the body' do
|
|
||||||
expect(mail.body.encoded).to match('brief summary')
|
|
||||||
expect(mail.body.encoded).to include 'The body of the foreign status'
|
|
||||||
expect(mail.body.encoded).to include sender.username
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'includes activities since the receiver last signed in' do
|
|
||||||
receiver.update!(last_emailed_at: nil, current_sign_in_at: '2000-03-01T00:00:00Z')
|
|
||||||
mail = NotificationMailer.digest(receiver.account)
|
|
||||||
expect(mail.body.encoded).to include 'Mar 01, 2000, 00:00'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe DigestMailerWorker do
|
|
||||||
describe 'perform' do
|
|
||||||
let(:user) { Fabricate(:user, last_emailed_at: 3.days.ago) }
|
|
||||||
|
|
||||||
context 'for a user who receives digests' do
|
|
||||||
it 'sends the email' do
|
|
||||||
service = double(deliver_now!: nil)
|
|
||||||
allow(NotificationMailer).to receive(:digest).and_return(service)
|
|
||||||
update_user_digest_setting(true)
|
|
||||||
described_class.perform_async(user.id)
|
|
||||||
|
|
||||||
expect(NotificationMailer).to have_received(:digest)
|
|
||||||
expect(user.reload.last_emailed_at).to be_within(1).of(Time.now.utc)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for a user who does not receive digests' do
|
|
||||||
it 'does not send the email' do
|
|
||||||
allow(NotificationMailer).to receive(:digest)
|
|
||||||
update_user_digest_setting(false)
|
|
||||||
described_class.perform_async(user.id)
|
|
||||||
|
|
||||||
expect(NotificationMailer).not_to have_received(:digest)
|
|
||||||
expect(user.last_emailed_at).to be_within(1).of(3.days.ago)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_user_digest_setting(value)
|
|
||||||
user.settings['notification_emails'] = user.settings['notification_emails'].merge('digest' => value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in New Issue
Block a user