diff options
author | Claire <claire.github-309c@sitedethib.com> | 2022-08-28 13:27:17 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-28 13:27:17 +0200 |
commit | 215738bb3cba4951e84174b461da5f1004b0649e (patch) | |
tree | ceca848b6a0553474e2b228dbf8c9e9c144cc99f /app | |
parent | 54d9a9c18a74a1ec766d8f611ad3ee11ab4c5422 (diff) | |
parent | 54ae7a221e862990550850500d70997c70187b70 (diff) |
Merge pull request #1833 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
Diffstat (limited to 'app')
52 files changed, 723 insertions, 519 deletions
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 46c9aba91..40bf685c5 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -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') diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index 3e502ccc4..d76aa745b 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -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 diff --git a/app/controllers/admin/users/roles_controller.rb b/app/controllers/admin/users/roles_controller.rb index 0db50cee9..f5dfc643d 100644 --- a/app/controllers/admin/users/roles_controller.rb +++ b/app/controllers/admin/users/roles_controller.rb @@ -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 diff --git a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb new file mode 100644 index 000000000..bf8a6a131 --- /dev/null +++ b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb @@ -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 diff --git a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb new file mode 100644 index 000000000..ac16f70b0 --- /dev/null +++ b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb @@ -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 diff --git a/app/controllers/api/v1/admin/ip_blocks_controller.rb b/app/controllers/api/v1/admin/ip_blocks_controller.rb new file mode 100644 index 000000000..f13d63267 --- /dev/null +++ b/app/controllers/api/v1/admin/ip_blocks_controller.rb @@ -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 diff --git a/app/controllers/concerns/accountable_concern.rb b/app/controllers/concerns/accountable_concern.rb index 87d62478d..c1349915f 100644 --- a/app/controllers/concerns/accountable_concern.rb +++ b/app/controllers/concerns/accountable_concern.rb @@ -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 diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 669ed00c6..4c1336436 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -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 diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 47eeeaac3..fd1977ac5 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -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 diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index c1b9f07a4..c84f566a3 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -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(); + } + } } }); diff --git a/app/javascript/flavours/glitch/util/main.js b/app/javascript/flavours/glitch/util/main.js index 6577b70c2..9e76774b6 100644 --- a/app/javascript/flavours/glitch/util/main.js +++ b/app/javascript/flavours/glitch/util/main.js @@ -1,9 +1,9 @@ -import * as registerPushNotifications from 'flavours/glitch/actions/push_notifications'; -import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications'; -import { default as Mastodon, store } from 'flavours/glitch/containers/mastodon'; import React from 'react'; import ReactDOM from 'react-dom'; -import ready from './ready'; +import * as registerPushNotifications from 'flavours/glitch/actions/push_notifications'; +import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications'; +import Mastodon, { store } from 'flavours/glitch/containers/mastodon'; +import ready from 'flavours/glitch/util/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()'); }); diff --git a/app/javascript/mastodon/components/timeline_hint.js b/app/javascript/mastodon/components/timeline_hint.js index fb55a62cc..ac9a79dcc 100644 --- a/app/javascript/mastodon/components/timeline_hint.js +++ b/app/javascript/mastodon/components/timeline_hint.js @@ -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> ); diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js index bda51f692..a66975bfd 100644 --- a/app/javascript/mastodon/main.js +++ b/app/javascript/mastodon/main.js @@ -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()'); }); diff --git a/app/javascript/mastodon/service_worker/entry.js b/app/javascript/mastodon/service_worker/entry.js index b354f3b33..e4c66cc00 100644 --- a/app/javascript/mastodon/service_worker/entry.js +++ b/app/javascript/mastodon/service_worker/entry.js @@ -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); diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js index 48a2be7e7..9b75e9b9d 100644 --- a/app/javascript/mastodon/service_worker/web_push_notifications.js +++ b/app/javascript/mastodon/service_worker/web_push_notifications.js @@ -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); diff --git a/app/javascript/mastodon/storage/db.js b/app/javascript/mastodon/storage/db.js deleted file mode 100644 index 377a792a7..000000000 --- a/app/javascript/mastodon/storage/db.js +++ /dev/null @@ -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'); - }; -}); diff --git a/app/javascript/mastodon/storage/modifier.js b/app/javascript/mastodon/storage/modifier.js deleted file mode 100644 index 9fadabef4..000000000 --- a/app/javascript/mastodon/storage/modifier.js +++ /dev/null @@ -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(); - })); - }); -} diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 431b8a73a..39211910f 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -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%); diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 9e683b6a1..ab73826ab 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -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) diff --git a/app/models/account.rb b/app/models/account.rb index 9627cc608..f75750838 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -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 diff --git a/app/models/account_warning.rb b/app/models/account_warning.rb index 6067b54b7..961a078b9 100644 --- a/app/models/account_warning.rb +++ b/app/models/account_warning.rb @@ -43,4 +43,8 @@ class AccountWarning < ApplicationRecord def overruled? overruled_at.present? end + + def to_log_human_identifier + target_account.acct + end end diff --git a/app/models/admin/action_log.rb b/app/models/admin/action_log.rb index 401bfd9ac..4fa8008f5 100644 --- a/app/models/admin/action_log.rb +++ b/app/models/admin/action_log.rb @@ -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 diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb index 0f2f712a2..c7a7e1a4c 100644 --- a/app/models/admin/action_log_filter.rb +++ b/app/models/admin/action_log_filter.rb @@ -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 diff --git a/app/models/announcement.rb b/app/models/announcement.rb index f8183aabc..bedced9de 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -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 diff --git a/app/models/appeal.rb b/app/models/appeal.rb index 1f32cfa8b..6fbf60b39 100644 --- a/app/models/appeal.rb +++ b/app/models/appeal.rb @@ -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 diff --git a/app/models/canonical_email_block.rb b/app/models/canonical_email_block.rb index 94781386c..1eb69ac67 100644 --- a/app/models/canonical_email_block.rb +++ b/app/models/canonical_email_block.rb @@ -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 diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index c89bf0586..9bf9860db 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -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? diff --git a/app/models/custom_filter_status.rb b/app/models/custom_filter_status.rb index b6bea1394..e748d6963 100644 --- a/app/models/custom_filter_status.rb +++ b/app/models/custom_filter_status.rb @@ -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 # diff --git a/app/models/domain_allow.rb b/app/models/domain_allow.rb index 7a0acbe32..9e746b915 100644 --- a/app/models/domain_allow.rb +++ b/app/models/domain_allow.rb @@ -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? diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index a15206b5e..b08687787 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -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] diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb index f9d74332b..10a0e5102 100644 --- a/app/models/email_domain_block.rb +++ b/app/models/email_domain_block.rb @@ -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 diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index dcf155840..5cfcf7205 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -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 diff --git a/app/models/instance.rb b/app/models/instance.rb index 36110ee40..edbf02a6d 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -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) diff --git a/app/models/ip_block.rb b/app/models/ip_block.rb index e1ab59806..8666f4248 100644 --- a/app/models/ip_block.rb +++ b/app/models/ip_block.rb @@ -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)) } diff --git a/app/models/report.rb b/app/models/report.rb index 2efb6d4a7..42c869dd4 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -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 diff --git a/app/models/status.rb b/app/models/status.rb index 3efa23ae2..c1e8862ca 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -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 diff --git a/app/models/unavailable_domain.rb b/app/models/unavailable_domain.rb index 5e8870bde..dfc0ef14e 100644 --- a/app/models/unavailable_domain.rb +++ b/app/models/unavailable_domain.rb @@ -16,6 +16,10 @@ class UnavailableDomain < ApplicationRecord after_commit :reset_cache! + def to_log_human_identifier + domain + end + private def reset_cache! diff --git a/app/models/user.rb b/app/models/user.rb index 46f66526e..962ca68ff 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/models/user_role.rb b/app/models/user_role.rb index 57a56c0b0..74dfdc220 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -155,6 +155,10 @@ class UserRole < ApplicationRecord end end + def to_log_human_identifier + name + end + private def in_permissions?(privilege) diff --git a/app/policies/canonical_email_block_policy.rb b/app/policies/canonical_email_block_policy.rb new file mode 100644 index 000000000..8d76075c9 --- /dev/null +++ b/app/policies/canonical_email_block_policy.rb @@ -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 diff --git a/app/policies/ip_block_policy.rb b/app/policies/ip_block_policy.rb index 1abc97ad8..2986a4fdb 100644 --- a/app/policies/ip_block_policy.rb +++ b/app/policies/ip_block_policy.rb @@ -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 diff --git a/app/serializers/rest/admin/canonical_email_block_serializer.rb b/app/serializers/rest/admin/canonical_email_block_serializer.rb new file mode 100644 index 000000000..fe385940a --- /dev/null +++ b/app/serializers/rest/admin/canonical_email_block_serializer.rb @@ -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 diff --git a/app/serializers/rest/admin/email_domain_block_serializer.rb b/app/serializers/rest/admin/email_domain_block_serializer.rb new file mode 100644 index 000000000..a026ff680 --- /dev/null +++ b/app/serializers/rest/admin/email_domain_block_serializer.rb @@ -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 diff --git a/app/serializers/rest/admin/ip_block_serializer.rb b/app/serializers/rest/admin/ip_block_serializer.rb new file mode 100644 index 000000000..6a38f8b56 --- /dev/null +++ b/app/serializers/rest/admin/ip_block_serializer.rb @@ -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 diff --git a/app/services/clear_domain_media_service.rb b/app/services/clear_domain_media_service.rb index 704cfb71a..9e70ebe51 100644 --- a/app/services/clear_domain_media_service.rb +++ b/app/services/clear_domain_media_service.rb @@ -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 = [] + + 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 - attachment.file.destroy if attachment.file&.exists? - attachment.type = :unknown - attachment.save + invalidate_association_caches!(affected_status_ids) unless affected_status_ids.empty? end end diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index e0879fcb6..9571f27b4 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -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' diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 40c38cecb..5cbab8fc5 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -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 diff --git a/app/views/notification_mailer/digest.html.haml b/app/views/notification_mailer/digest.html.haml deleted file mode 100644 index a94ace228..000000000 --- a/app/views/notification_mailer/digest.html.haml +++ /dev/null @@ -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) diff --git a/app/views/notification_mailer/digest.text.erb b/app/views/notification_mailer/digest.text.erb deleted file mode 100644 index 0f84a4ef0..000000000 --- a/app/views/notification_mailer/digest.text.erb +++ /dev/null @@ -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 %> diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index 943e21b50..a03faa145 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -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 diff --git a/app/workers/digest_mailer_worker.rb b/app/workers/digest_mailer_worker.rb deleted file mode 100644 index 21f1c357a..000000000 --- a/app/workers/digest_mailer_worker.rb +++ /dev/null @@ -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 diff --git a/app/workers/scheduler/email_scheduler.rb b/app/workers/scheduler/email_scheduler.rb deleted file mode 100644 index c052f2fce..000000000 --- a/app/workers/scheduler/email_scheduler.rb +++ /dev/null @@ -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 |