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:
@ -16,7 +16,11 @@ module Admin
|
||||
def batch
|
||||
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
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||
|
@ -23,6 +23,7 @@ module Admin
|
||||
@role.current_account = current_account
|
||||
|
||||
if @role.save
|
||||
log_action :create, @role
|
||||
redirect_to admin_roles_path
|
||||
else
|
||||
render :new
|
||||
@ -39,6 +40,7 @@ module Admin
|
||||
@role.current_account = current_account
|
||||
|
||||
if @role.update(resource_params)
|
||||
log_action :update, @role
|
||||
redirect_to admin_roles_path
|
||||
else
|
||||
render :edit
|
||||
@ -48,6 +50,7 @@ module Admin
|
||||
def destroy
|
||||
authorize @role, :destroy?
|
||||
@role.destroy!
|
||||
log_action :destroy, @role
|
||||
redirect_to admin_roles_path
|
||||
end
|
||||
|
||||
|
@ -14,6 +14,7 @@ module Admin
|
||||
@user.current_account = current_account
|
||||
|
||||
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')
|
||||
else
|
||||
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
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def log_action(action, target, options = {})
|
||||
Admin::ActionLog.create(account: current_account, action: action, target: target, recorded_changes: options.stringify_keys)
|
||||
def log_action(action, target)
|
||||
Admin::ActionLog.create(
|
||||
account: current_account,
|
||||
action: action,
|
||||
target: target
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -58,7 +58,7 @@ class Settings::PreferencesController < Settings::BaseController
|
||||
:setting_trends,
|
||||
:setting_crop_images,
|
||||
: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)
|
||||
)
|
||||
end
|
||||
|
@ -2,64 +2,29 @@
|
||||
|
||||
module Admin::ActionLogsHelper
|
||||
def log_target(log)
|
||||
if log.target
|
||||
linkable_log_target(log.target)
|
||||
else
|
||||
log_target_from_history(log.target_type, log.recorded_changes)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def linkable_log_target(record)
|
||||
case record.class.name
|
||||
case log.target_type
|
||||
when 'Account'
|
||||
link_to record.acct, admin_account_path(record.id)
|
||||
link_to log.human_identifier, admin_account_path(log.target_id)
|
||||
when 'User'
|
||||
link_to record.account.acct, admin_account_path(record.account_id)
|
||||
when 'CustomEmoji'
|
||||
record.shortcode
|
||||
link_to log.human_identifier, admin_account_path(log.route_param)
|
||||
when 'UserRole'
|
||||
link_to log.human_identifier, admin_roles_path(log.target_id)
|
||||
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'
|
||||
link_to record.domain, "https://#{record.domain}"
|
||||
link_to log.human_identifier, "https://#{log.human_identifier}"
|
||||
when 'Status'
|
||||
link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record)
|
||||
link_to log.human_identifier, log.permalink
|
||||
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'
|
||||
link_to truncate(record.text), edit_admin_announcement_path(record.id)
|
||||
when 'IpBlock'
|
||||
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
|
||||
when 'Instance'
|
||||
record.domain
|
||||
link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id)
|
||||
when 'IpBlock', 'Instance', 'CustomEmoji'
|
||||
log.human_identifier
|
||||
when 'CanonicalEmailBlock'
|
||||
content_tag(:samp, log.human_identifier[0...7], title: log.human_identifier)
|
||||
when 'Appeal'
|
||||
link_to record.account.acct, disputes_strike_path(record.strike)
|
||||
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']
|
||||
link_to log.human_identifier, disputes_strike_path(log.route_param)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -6,18 +6,71 @@ import ready from '../mastodon/ready';
|
||||
|
||||
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 }) => {
|
||||
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
|
||||
|
||||
[].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => {
|
||||
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', () => {
|
||||
const checkAllElement = document.querySelector('#batch_checkbox_all');
|
||||
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
|
||||
|
||||
if (checkAllElement) {
|
||||
checkAllElement.checked = [].every.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'>
|
||||
<strong><FormattedMessage id='timeline_hint.remote_resource_not_displayed' defaultMessage='{resource} from other servers are not displayed.' values={{ resource }} /></strong>
|
||||
<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>
|
||||
);
|
||||
|
||||
|
@ -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 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');
|
||||
|
||||
@ -24,10 +24,20 @@ function main() {
|
||||
|
||||
ReactDOM.render(<Mastodon {...props} />, mountNode);
|
||||
store.dispatch(setupBrowserNotifications());
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// avoid offline in dev mode because it's harder to debug
|
||||
require('offline-plugin/runtime').install();
|
||||
store.dispatch(registerPushNotifications.register());
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
import('workbox-window')
|
||||
.then(({ Workbox }) => {
|
||||
const wb = new Workbox('/sw.js');
|
||||
|
||||
return wb.register();
|
||||
})
|
||||
.then(() => {
|
||||
store.dispatch(registerPushNotifications.register());
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
perf.stop('main()');
|
||||
});
|
||||
|
@ -1,20 +1,59 @@
|
||||
// import { freeStorage, storageFreeable } from '../storage/modifier';
|
||||
import './web_push_notifications';
|
||||
import { ExpirationPlugin } from 'workbox-expiration';
|
||||
import { precacheAndRoute } from 'workbox-precaching';
|
||||
import { registerRoute } from 'workbox-routing';
|
||||
import { CacheFirst } from 'workbox-strategies';
|
||||
import { handleNotificationClick, handlePush } from './web_push_notifications';
|
||||
|
||||
// function openSystemCache() {
|
||||
// return caches.open('mastodon-system');
|
||||
// }
|
||||
const CACHE_NAME_PREFIX = 'mastodon-';
|
||||
|
||||
function openWebCache() {
|
||||
return caches.open('mastodon-web');
|
||||
return caches.open(`${CACHE_NAME_PREFIX}web`);
|
||||
}
|
||||
|
||||
function fetchRoot() {
|
||||
return fetch('/', { credentials: 'include', redirect: 'manual' });
|
||||
}
|
||||
|
||||
// const firefox = navigator.userAgent.match(/Firefox\/(\d+)/);
|
||||
// const invalidOnlyIfCached = firefox && firefox[1] < 60;
|
||||
precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
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
|
||||
// that is already installed, and replace the currently active worker on open pages.
|
||||
@ -52,26 +91,8 @@ self.addEventListener('fetch', function(event) {
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
return cached;
|
||||
});
|
||||
}));
|
||||
} */
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('push', handlePush);
|
||||
self.addEventListener('notificationclick', handleNotificationClick);
|
||||
|
@ -75,7 +75,7 @@ const formatMessage = (messageId, locale, values = {}) =>
|
||||
const htmlToPlainText = html =>
|
||||
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();
|
||||
|
||||
// Placeholder until more information can be loaded
|
||||
@ -189,7 +189,7 @@ const openUrl = url =>
|
||||
return self.clients.openWindow(url);
|
||||
});
|
||||
|
||||
const handleNotificationClick = (event) => {
|
||||
export const handleNotificationClick = (event) => {
|
||||
const reactToNotificationClick = new Promise((resolve, reject) => {
|
||||
if (event.action) {
|
||||
if (event.action === 'expand') {
|
||||
@ -211,6 +211,3 @@ const handleNotificationClick = (event) => {
|
||||
|
||||
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 {
|
||||
padding: 16px;
|
||||
border: 1px solid darken($ui-base-color, 8%);
|
||||
|
@ -60,7 +60,7 @@ class FeedManager
|
||||
# @param [Boolean] update
|
||||
# @return [Boolean]
|
||||
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)
|
||||
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
|
||||
# @return [Boolean]
|
||||
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
|
||||
true
|
||||
@ -85,7 +85,7 @@ class FeedManager
|
||||
# @param [Boolean] update
|
||||
# @return [Boolean]
|
||||
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)
|
||||
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
|
||||
# @return [Boolean]
|
||||
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
|
||||
true
|
||||
@ -133,7 +133,7 @@ class FeedManager
|
||||
# @return [void]
|
||||
def merge_into_home(from_account, into_account)
|
||||
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)
|
||||
|
||||
if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
|
||||
@ -159,7 +159,7 @@ class FeedManager
|
||||
# @return [void]
|
||||
def merge_into_list(from_account, list)
|
||||
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)
|
||||
|
||||
if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
|
||||
@ -188,7 +188,7 @@ class FeedManager
|
||||
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|
|
||||
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
|
||||
|
||||
@ -201,7 +201,7 @@ class FeedManager
|
||||
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|
|
||||
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
|
||||
|
||||
@ -260,7 +260,7 @@ class FeedManager
|
||||
# @return [void]
|
||||
def populate_home(account)
|
||||
limit = FeedManager::MAX_ITEMS / 2
|
||||
aggregate = account.user&.aggregates_reblogs?
|
||||
aggregate = true
|
||||
timeline_key = key(:home, account.id)
|
||||
|
||||
account.statuses.limit(limit).each do |status|
|
||||
|
@ -66,24 +66,6 @@ class NotificationMailer < ApplicationMailer
|
||||
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
|
||||
|
||||
def thread_by_conversation(conversation)
|
||||
|
@ -364,6 +364,10 @@ class Account < ApplicationRecord
|
||||
username
|
||||
end
|
||||
|
||||
def to_log_human_identifier
|
||||
acct
|
||||
end
|
||||
|
||||
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) }
|
||||
end
|
||||
|
@ -43,4 +43,8 @@ class AccountWarning < ApplicationRecord
|
||||
def overruled?
|
||||
overruled_at.present?
|
||||
end
|
||||
|
||||
def to_log_human_identifier
|
||||
target_account.acct
|
||||
end
|
||||
end
|
||||
|
@ -9,38 +9,42 @@
|
||||
# action :string default(""), not null
|
||||
# target_type :string
|
||||
# target_id :bigint(8)
|
||||
# recorded_changes :text default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# human_identifier :string
|
||||
# route_param :string
|
||||
# permalink :string
|
||||
#
|
||||
|
||||
class Admin::ActionLog < ApplicationRecord
|
||||
serialize :recorded_changes
|
||||
self.ignored_columns = %w(
|
||||
recorded_changes
|
||||
)
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :target, polymorphic: true, optional: true
|
||||
|
||||
default_scope -> { order('id desc') }
|
||||
|
||||
before_validation :set_human_identifier
|
||||
before_validation :set_route_param
|
||||
before_validation :set_permalink
|
||||
|
||||
def action
|
||||
super.to_sym
|
||||
end
|
||||
|
||||
before_validation :set_changes
|
||||
|
||||
private
|
||||
|
||||
def set_changes
|
||||
case action
|
||||
when :destroy, :create
|
||||
self.recorded_changes = target.attributes
|
||||
when :update, :promote, :demote
|
||||
self.recorded_changes = target.previous_changes
|
||||
when :change_email
|
||||
self.recorded_changes = ActiveSupport::HashWithIndifferentAccess.new(
|
||||
email: [target.email, nil],
|
||||
unconfirmed_email: [nil, target.unconfirmed_email]
|
||||
)
|
||||
end
|
||||
def set_human_identifier
|
||||
self.human_identifier = target.to_log_human_identifier if target.respond_to?(:to_log_human_identifier)
|
||||
end
|
||||
|
||||
def set_route_param
|
||||
self.route_param = target.to_log_route_param if target.respond_to?(:to_log_route_param)
|
||||
end
|
||||
|
||||
def set_permalink
|
||||
self.permalink = target.to_log_permalink if target.respond_to?(:to_log_permalink)
|
||||
end
|
||||
end
|
||||
|
@ -12,6 +12,7 @@ class Admin::ActionLogFilter
|
||||
reject_appeal: { target_type: 'Appeal', action: 'reject' }.freeze,
|
||||
assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.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,
|
||||
approve_user: { target_type: 'User', action: 'approve' }.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_block: { target_type: 'DomainBlock', 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_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,
|
||||
destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze,
|
||||
destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze,
|
||||
destroy_domain_allow: { target_type: 'DomainAllow', 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_instance: { target_type: 'Instance', action: 'destroy' }.freeze,
|
||||
destroy_unavailable_domain: { target_type: 'UnavailableDomain', 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_custom_emoji: { target_type: 'CustomEmoji', 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_custom_emoji: { target_type: 'CustomEmoji', 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,
|
||||
}.freeze
|
||||
|
||||
|
@ -34,6 +34,10 @@ class Announcement < ApplicationRecord
|
||||
before_validation :set_all_day
|
||||
before_validation :set_published, on: :create
|
||||
|
||||
def to_log_human_identifier
|
||||
text
|
||||
end
|
||||
|
||||
def publish!
|
||||
update!(published: true, published_at: Time.now.utc, scheduled_at: nil)
|
||||
end
|
||||
|
@ -52,6 +52,14 @@ class Appeal < ApplicationRecord
|
||||
update!(rejected_at: Time.now.utc, rejected_by_account: current_account)
|
||||
end
|
||||
|
||||
def to_log_human_identifier
|
||||
account.acct
|
||||
end
|
||||
|
||||
def to_log_route_param
|
||||
account_warning_id
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_time_frame
|
||||
|
@ -5,27 +5,30 @@
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# canonical_email_hash :string default(""), not null
|
||||
# reference_account_id :bigint(8) not null
|
||||
# reference_account_id :bigint(8)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class CanonicalEmailBlock < ApplicationRecord
|
||||
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
|
||||
|
||||
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)
|
||||
self.canonical_email_hash = email_to_canonical_email_hash(email)
|
||||
end
|
||||
|
||||
def self.block?(email)
|
||||
where(canonical_email_hash: email_to_canonical_email_hash(email)).exists?
|
||||
end
|
||||
|
||||
def self.find_blocks(email)
|
||||
where(canonical_email_hash: email_to_canonical_email_hash(email))
|
||||
matching_email(email).exists?
|
||||
end
|
||||
end
|
||||
|
@ -49,7 +49,7 @@ class CustomEmoji < ApplicationRecord
|
||||
scope :local, -> { where(domain: nil) }
|
||||
scope :remote, -> { where.not(domain: nil) }
|
||||
scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) }
|
||||
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
|
||||
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches("%.#{domain}"))) }
|
||||
scope :listed, -> { local.where(disabled: false).where(visible_in_picker: true) }
|
||||
|
||||
remotable_attachment :image, LIMIT
|
||||
@ -70,6 +70,10 @@ class CustomEmoji < ApplicationRecord
|
||||
copy.tap(&:save!)
|
||||
end
|
||||
|
||||
def to_log_human_identifier
|
||||
shortcode
|
||||
end
|
||||
|
||||
class << self
|
||||
def from_text(text, domain = nil)
|
||||
return [] if text.blank?
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# 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
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
@ -19,6 +19,10 @@ class DomainAllow < ApplicationRecord
|
||||
|
||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||
|
||||
def to_log_human_identifier
|
||||
domain
|
||||
end
|
||||
|
||||
class << self
|
||||
def allowed?(domain)
|
||||
!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 :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
|
||||
if suspend?
|
||||
[:suspend]
|
||||
|
@ -17,6 +17,7 @@ class EmailDomainBlock < ApplicationRecord
|
||||
)
|
||||
|
||||
include DomainNormalizable
|
||||
include Paginable
|
||||
|
||||
belongs_to :parent, class_name: 'EmailDomainBlock', optional: true
|
||||
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
|
||||
attr_accessor :other_domains
|
||||
|
||||
def to_log_human_identifier
|
||||
domain
|
||||
end
|
||||
|
||||
def history
|
||||
@history ||= Trends::History.new('email_domain_blocks', id)
|
||||
end
|
||||
|
@ -6,7 +6,8 @@ class Form::AccountBatch
|
||||
include AccountableConcern
|
||||
include Payloadable
|
||||
|
||||
attr_accessor :account_ids, :action, :current_account
|
||||
attr_accessor :account_ids, :action, :current_account,
|
||||
:select_all_matching, :query
|
||||
|
||||
def save
|
||||
case action
|
||||
@ -60,7 +61,11 @@ class Form::AccountBatch
|
||||
end
|
||||
|
||||
def accounts
|
||||
Account.where(id: account_ids)
|
||||
if select_all_matching?
|
||||
query
|
||||
else
|
||||
Account.where(id: account_ids)
|
||||
end
|
||||
end
|
||||
|
||||
def approve!
|
||||
@ -101,7 +106,7 @@ class Form::AccountBatch
|
||||
|
||||
def reject_account(account)
|
||||
authorize(account.user, :reject?)
|
||||
log_action(:reject, account.user, username: account.username)
|
||||
log_action(:reject, account.user)
|
||||
account.suspend!(origin: :local)
|
||||
AccountDeletionWorker.perform_async(account.id, { 'reserve_username' => false })
|
||||
end
|
||||
@ -118,4 +123,8 @@ class Form::AccountBatch
|
||||
log_action(:approve, account.user)
|
||||
account.user.approve!
|
||||
end
|
||||
|
||||
def select_all_matching?
|
||||
select_all_matching == '1'
|
||||
end
|
||||
end
|
||||
|
@ -48,6 +48,8 @@ class Instance < ApplicationRecord
|
||||
domain
|
||||
end
|
||||
|
||||
alias to_log_human_identifier to_param
|
||||
|
||||
delegate :exhausted_deliveries_days, to: :delivery_failure_tracker
|
||||
|
||||
def availability_over_days(num_days, end_date = Time.now.utc.to_date)
|
||||
|
@ -16,6 +16,7 @@ class IpBlock < ApplicationRecord
|
||||
CACHE_KEY = 'blocked_ips'
|
||||
|
||||
include Expireable
|
||||
include Paginable
|
||||
|
||||
enum severity: {
|
||||
sign_up_requires_approval: 5000,
|
||||
@ -27,6 +28,10 @@ class IpBlock < ApplicationRecord
|
||||
|
||||
after_commit :reset_cache
|
||||
|
||||
def to_log_human_identifier
|
||||
"#{ip}/#{ip.prefix}"
|
||||
end
|
||||
|
||||
class << self
|
||||
def blocked?(remote_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?
|
||||
end
|
||||
|
||||
def to_log_human_identifier
|
||||
id
|
||||
end
|
||||
|
||||
def history
|
||||
subquery = [
|
||||
Admin::ActionLog.where(
|
||||
@ -136,6 +140,8 @@ class Report < ApplicationRecord
|
||||
Admin::ActionLog.from(Arel::Nodes::As.new(subquery, Admin::ActionLog.arel_table))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_uri
|
||||
self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? && account.local?
|
||||
end
|
||||
|
@ -171,6 +171,14 @@ class Status < ApplicationRecord
|
||||
].compact.join("\n\n")
|
||||
end
|
||||
|
||||
def to_log_human_identifier
|
||||
account.acct
|
||||
end
|
||||
|
||||
def to_log_permalink
|
||||
ActivityPub::TagManager.instance.uri_for(self)
|
||||
end
|
||||
|
||||
def reply?
|
||||
!in_reply_to_id.nil? || attributes['reply']
|
||||
end
|
||||
|
@ -16,6 +16,10 @@ class UnavailableDomain < ApplicationRecord
|
||||
|
||||
after_commit :reset_cache!
|
||||
|
||||
def to_log_human_identifier
|
||||
domain
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_cache!
|
||||
|
@ -181,6 +181,14 @@ class User < ApplicationRecord
|
||||
update!(disabled: false)
|
||||
end
|
||||
|
||||
def to_log_human_identifier
|
||||
account.acct
|
||||
end
|
||||
|
||||
def to_log_route_param
|
||||
account_id
|
||||
end
|
||||
|
||||
def confirm
|
||||
new_user = !confirmed?
|
||||
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')
|
||||
end
|
||||
|
||||
def allows_digest_emails?
|
||||
settings.notification_emails['digest']
|
||||
end
|
||||
|
||||
def allows_report_emails?
|
||||
settings.notification_emails['report']
|
||||
end
|
||||
|
@ -155,6 +155,10 @@ class UserRole < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def to_log_human_identifier
|
||||
name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
def update?
|
||||
role.can?(:manage_blocks)
|
||||
end
|
||||
|
||||
def destroy?
|
||||
role.can?(:manage_blocks)
|
||||
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
|
||||
|
||||
def invalidate_association_caches!
|
||||
def invalidate_association_caches!(status_ids)
|
||||
# Normally, associated models of a status are immutable (except for accounts)
|
||||
# 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
|
||||
# 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
|
||||
|
||||
def clear_media!
|
||||
@affected_status_ids = []
|
||||
|
||||
begin
|
||||
clear_account_images!
|
||||
clear_account_attachments!
|
||||
clear_emojos!
|
||||
ensure
|
||||
invalidate_association_caches!
|
||||
end
|
||||
clear_account_images!
|
||||
clear_account_attachments!
|
||||
clear_emojos!
|
||||
end
|
||||
|
||||
def clear_account_images!
|
||||
@ -39,12 +33,18 @@ class ClearDomainMediaService < BaseService
|
||||
end
|
||||
|
||||
def clear_account_attachments!
|
||||
media_from_blocked_domain.reorder(nil).find_each do |attachment|
|
||||
@affected_status_ids << attachment.status_id if attachment.status_id.present?
|
||||
media_from_blocked_domain.reorder(nil).find_in_batches do |attachments|
|
||||
affected_status_ids = []
|
||||
|
||||
attachment.file.destroy if attachment.file&.exists?
|
||||
attachment.type = :unknown
|
||||
attachment.save
|
||||
attachments.each do |attachment|
|
||||
affected_status_ids << attachment.status_id if attachment.status_id.present?
|
||||
|
||||
attachment.file.destroy if attachment.file&.exists?
|
||||
attachment.type = :unknown
|
||||
attachment.save
|
||||
end
|
||||
|
||||
invalidate_association_caches!(affected_status_ids) unless affected_status_ids.empty?
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -34,6 +34,7 @@
|
||||
|
||||
= form_for(@form, url: batch_admin_accounts_path) do |f|
|
||||
= hidden_field_tag :page, params[:page] || 1
|
||||
= hidden_field_tag :select_all_matching, '0'
|
||||
|
||||
- AccountFilter::KEYS.each do |key|
|
||||
= 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('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
|
||||
- if @accounts.empty?
|
||||
= nothing_here 'nothing-here--under-tabs'
|
||||
|
@ -23,6 +23,7 @@
|
||||
%link{ rel: 'manifest', href: manifest_path(format: :json) }/
|
||||
%meta{ name: 'theme-color', content: '#6364FF' }/
|
||||
%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
|
||||
|
||||
|
@ -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
|
||||
= 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'
|
||||
|
||||
.fields-group
|
||||
|
@ -10,9 +10,6 @@
|
||||
.fields-group
|
||||
= 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
|
||||
.fields-group
|
||||
= 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
|
Reference in New Issue
Block a user