Merge branch 'master' into glitch-soc/master

Conflicts:
	config/routes.rb

Added the “endorsements” route from upstream.
main
Thibaut Girka 2018-08-21 18:24:48 +02:00
commit 8b4abaa90d
41 changed files with 215 additions and 78 deletions

View File

@ -16,7 +16,6 @@ gem 'dotenv-rails', '~> 2.2', '< 2.3'
gem 'aws-sdk-s3', '~> 1.9', require: false gem 'aws-sdk-s3', '~> 1.9', require: false
gem 'fog-core', '~> 1.45' gem 'fog-core', '~> 1.45'
gem 'fog-local', '~> 0.5', require: false
gem 'fog-openstack', '~> 0.1', require: false gem 'fog-openstack', '~> 0.1', require: false
gem 'paperclip', '~> 6.0' gem 'paperclip', '~> 6.0'
gem 'paperclip-av-transcoder', '~> 0.6' gem 'paperclip-av-transcoder', '~> 0.6'
@ -42,7 +41,7 @@ gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10' gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.2' gem 'omniauth', '~> 1.2'
gem 'doorkeeper', '~> 4.2', '< 4.3' gem 'doorkeeper', '~> 4.4'
gem 'fast_blank', '~> 1.0' gem 'fast_blank', '~> 1.0'
gem 'fastimage' gem 'fastimage'
gem 'goldfinger', '~> 2.1' gem 'goldfinger', '~> 2.1'

View File

@ -181,7 +181,7 @@ GEM
docile (1.3.0) docile (1.3.0)
domain_name (0.5.20180417) domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.6) doorkeeper (4.4.2)
railties (>= 4.2) railties (>= 4.2)
dotenv (2.2.2) dotenv (2.2.2)
dotenv-rails (2.2.2) dotenv-rails (2.2.2)
@ -220,8 +220,6 @@ GEM
fog-json (1.0.2) fog-json (1.0.2)
fog-core (~> 1.0) fog-core (~> 1.0)
multi_json (~> 1.10) multi_json (~> 1.10)
fog-local (0.5.0)
fog-core (>= 1.27, < 3.0)
fog-openstack (0.1.25) fog-openstack (0.1.25)
fog-core (~> 1.40) fog-core (~> 1.40)
fog-json (>= 1.0) fog-json (>= 1.0)
@ -674,14 +672,13 @@ DEPENDENCIES
devise (~> 4.4) devise (~> 4.4)
devise-two-factor (~> 3.0) devise-two-factor (~> 3.0)
devise_pam_authenticatable2 (~> 9.1) devise_pam_authenticatable2 (~> 9.1)
doorkeeper (~> 4.2, < 4.3) doorkeeper (~> 4.4)
dotenv-rails (~> 2.2, < 2.3) dotenv-rails (~> 2.2, < 2.3)
fabrication (~> 2.20) fabrication (~> 2.20)
faker (~> 1.8) faker (~> 1.8)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fastimage fastimage
fog-core (~> 1.45) fog-core (~> 1.45)
fog-local (~> 0.5)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
fuubar (~> 2.2) fuubar (~> 2.2)
goldfinger (~> 2.1) goldfinger (~> 2.1)

View File

@ -43,7 +43,7 @@ class AccountsController < ApplicationController
format.json do format.json do
skip_session! skip_session!
render_cached_json(['activitypub', 'actor', @account.cache_key], content_type: 'application/activity+json') do render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
end end
end end

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
class Api::V1::EndorsementsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
before_action :require_user!
after_action :insert_pagination_headers
respond_to :json
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
end
private
def load_accounts
if unlimited?
endorsed_accounts.all
else
endorsed_accounts.paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]
)
end
end
def endorsed_accounts
current_account.endorsed_accounts
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
return if unlimited?
if records_continue?
api_v1_endorsements_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
return if unlimited?
unless @accounts.empty?
api_v1_endorsements_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@accounts.last.id
end
def pagination_since_id
@accounts.first.id
end
def records_continue?
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end
def unlimited?
params[:limit] == '0'
end
end

View File

@ -17,8 +17,7 @@ class Api::V1::StatusesController < Api::BaseController
CONTEXT_LIMIT = 4_096 CONTEXT_LIMIT = 4_096
def show def show
cached = Rails.cache.read(@status.cache_key) @status = cache_collection([@status], Status).first
@status = cached unless cached.nil?
render json: @status, serializer: REST::StatusSerializer render json: @status, serializer: REST::StatusSerializer
end end

View File

@ -178,12 +178,8 @@ class ApplicationController < ActionController::Base
return raw unless klass.respond_to?(:with_includes) return raw unless klass.respond_to?(:with_includes)
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
uncached_ids = [] cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
cached_keys_with_value = Rails.cache.read_multi(*raw.map(&:cache_key)) uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
raw.each do |item|
uncached_ids << item.id unless cached_keys_with_value.key?(item.cache_key)
end
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!) klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
@ -191,11 +187,11 @@ class ApplicationController < ActionController::Base
uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h
uncached.each_value do |item| uncached.each_value do |item|
Rails.cache.write(item.cache_key, item) Rails.cache.write(item, item)
end end
end end
raw.map { |item| cached_keys_with_value[item.cache_key] || uncached[item.id] }.compact raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact
end end
def respond_with_error(code) def respond_with_error(code)
@ -211,7 +207,6 @@ class ApplicationController < ActionController::Base
def render_cached_json(cache_key, **options) def render_cached_json(cache_key, **options)
options[:expires_in] ||= 3.minutes options[:expires_in] ||= 3.minutes
cache_key = cache_key.join(':') if cache_key.is_a?(Enumerable)
cache_public = options.key?(:public) ? options.delete(:public) : true cache_public = options.key?(:public) ? options.delete(:public) : true
content_type = options.delete(:content_type) || 'application/json' content_type = options.delete(:content_type) || 'application/json'

View File

@ -9,7 +9,7 @@ class EmojisController < ApplicationController
format.json do format.json do
skip_session! skip_session!
render_cached_json(['activitypub', 'emoji', @emoji.cache_key], content_type: 'application/activity+json') do render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
end end
end end

View File

@ -34,7 +34,7 @@ class StatusesController < ApplicationController
format.json do format.json do
skip_session! unless @stream_entry.hidden? skip_session! unless @stream_entry.hidden?
render_cached_json(['activitypub', 'note', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
end end
end end
@ -44,7 +44,7 @@ class StatusesController < ApplicationController
def activity def activity
skip_session! skip_session!
render_cached_json(['activitypub', 'activity', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
end end
end end

View File

@ -55,7 +55,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
return; return;
} }
if (!params.max_id && timeline.get('items', ImmutableList()).size > 0) { if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) {
params.since_id = timeline.getIn(['items', 0]); params.since_id = timeline.getIn(['items', 0]);
} }

View File

@ -137,7 +137,7 @@ class DropdownMenu extends React.PureComponent {
// It should not be transformed when mounting because the resulting // It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by // size will be used to determine the coordinate of the menu by
// react-overlays // react-overlays
<div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}> <div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} /> <div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
<ul> <ul>

View File

@ -28,6 +28,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
style: PropTypes.object, style: PropTypes.object,
items: PropTypes.array.isRequired, items: PropTypes.array.isRequired,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
placement: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
}; };
@ -119,7 +120,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
render () { render () {
const { mounted } = this.state; const { mounted } = this.state;
const { style, items, value } = this.props; const { style, items, placement, value } = this.props;
return ( return (
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
@ -127,7 +128,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
// It should not be transformed when mounting because the resulting // It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by // size will be used to determine the coordinate of the menu by
// react-overlays // react-overlays
<div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}> <div className={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}>
{items.map(item => ( {items.map(item => (
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}> <div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
<div className='privacy-dropdown__option__icon'> <div className='privacy-dropdown__option__icon'>
@ -226,7 +227,7 @@ export default class PrivacyDropdown extends React.PureComponent {
const valueOption = this.options.find(item => item.value === value); const valueOption = this.options.find(item => item.value === value);
return ( return (
<div className={classNames('privacy-dropdown', { active: open })} onKeyDown={this.handleKeyDown}> <div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}>
<div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === 0 })}> <div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === 0 })}>
<IconButton <IconButton
className='privacy-dropdown__value-icon' className='privacy-dropdown__value-icon'
@ -247,6 +248,7 @@ export default class PrivacyDropdown extends React.PureComponent {
value={value} value={value}
onClose={this.handleClose} onClose={this.handleClose}
onChange={this.handleChange} onChange={this.handleChange}
placement={placement}
/> />
</Overlay> </Overlay>
</div> </div>

View File

@ -89,6 +89,7 @@ const keyMap = {
goToProfile: 'g u', goToProfile: 'g u',
goToBlocked: 'g b', goToBlocked: 'g b',
goToMuted: 'g m', goToMuted: 'g m',
goToRequests: 'g r',
toggleHidden: 'x', toggleHidden: 'x',
}; };
@ -427,6 +428,10 @@ export default class UI extends React.PureComponent {
this.context.router.history.push('/mutes'); this.context.router.history.push('/mutes');
} }
handleHotkeyGoToRequests = () => {
this.context.router.history.push('/follow_requests');
}
render () { render () {
const { draggingOver } = this.state; const { draggingOver } = this.state;
const { children, isComposing, location, dropdownMenuIsOpen } = this.props; const { children, isComposing, location, dropdownMenuIsOpen } = this.props;
@ -449,6 +454,7 @@ export default class UI extends React.PureComponent {
goToProfile: this.handleHotkeyGoToProfile, goToProfile: this.handleHotkeyGoToProfile,
goToBlocked: this.handleHotkeyGoToBlocked, goToBlocked: this.handleHotkeyGoToBlocked,
goToMuted: this.handleHotkeyGoToMuted, goToMuted: this.handleHotkeyGoToMuted,
goToRequests: this.handleHotkeyGoToRequests,
}; };
return ( return (

View File

@ -1,5 +1,6 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { me } from '../initial_state';
const getAccountBase = (state, id) => state.getIn(['accounts', id], null); const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null); const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
@ -83,7 +84,7 @@ export const makeGetStatus = () => {
statusReblog = null; statusReblog = null;
} }
const regex = regexFromFilters(filters); const regex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(filters);
const filtered = regex && regex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index')); const filtered = regex && regex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index'));
return statusBase.withMutations(map => { return statusBase.withMutations(map => {

View File

@ -230,7 +230,6 @@
.dropdown-menu { .dropdown-menu {
position: absolute; position: absolute;
transform-origin: 50% 0;
} }
.invisible { .invisible {
@ -1634,6 +1633,22 @@ a.account__display-name {
ul { ul {
list-style: none; list-style: none;
} }
&.left {
transform-origin: 100% 50%;
}
&.top {
transform-origin: 50% 100%;
}
&.bottom {
transform-origin: 50% 0;
}
&.right {
transform-origin: 0 50%;
}
} }
.dropdown-menu__arrow { .dropdown-menu__arrow {
@ -3300,7 +3315,14 @@ a.status-card {
border-radius: 4px; border-radius: 4px;
margin-left: 40px; margin-left: 40px;
overflow: hidden; overflow: hidden;
transform-origin: 50% 0;
&.top {
transform-origin: 50% 100%;
}
&.bottom {
transform-origin: 50% 0;
}
} }
.privacy-dropdown__option { .privacy-dropdown__option {
@ -3372,6 +3394,10 @@ a.status-card {
} }
} }
&.top .privacy-dropdown__value {
border-radius: 0 0 4px 4px;
}
.privacy-dropdown__dropdown { .privacy-dropdown__dropdown {
display: block; display: block;
box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1); box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1);
@ -4176,6 +4202,10 @@ a.status-card {
color: $highlight-text-color; color: $highlight-text-color;
} }
.status__content p {
color: $inverted-text-color;
}
@media screen and (max-width: 480px) { @media screen and (max-width: 480px) {
max-height: 10vh; max-height: 10vh;
} }

View File

@ -154,9 +154,8 @@ code {
margin-bottom: 15px; margin-bottom: 15px;
} }
li { ul {
float: left; columns: 2;
width: 50%;
} }
} }

View File

@ -11,6 +11,7 @@
# #
class AccountPin < ApplicationRecord class AccountPin < ApplicationRecord
include Paginable
include RelationshipCacheable include RelationshipCacheable
belongs_to :account belongs_to :account

View File

@ -23,7 +23,7 @@ class Form::StatusBatch
media_attached_status_ids = MediaAttachment.where(status_id: status_ids).pluck(:status_id) media_attached_status_ids = MediaAttachment.where(status_id: status_ids).pluck(:status_id)
ApplicationRecord.transaction do ApplicationRecord.transaction do
Status.where(id: media_attached_status_ids).find_each do |status| Status.where(id: media_attached_status_ids).reorder(nil).find_each do |status|
status.update!(sensitive: sensitive) status.update!(sensitive: sensitive)
log_action :update, status log_action :update, status
end end
@ -35,7 +35,7 @@ class Form::StatusBatch
end end
def delete_statuses def delete_statuses
Status.where(id: status_ids).find_each do |status| Status.where(id: status_ids).reorder(nil).find_each do |status|
RemovalWorker.perform_async(status.id) RemovalWorker.perform_async(status.id)
log_action :destroy, status log_action :destroy, status
end end

View File

@ -39,8 +39,6 @@ class Notification < ApplicationRecord
validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] } validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values } validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
scope :cache_ids, -> { select(:id, :updated_at, :activity_type, :activity_id) }
scope :browserable, ->(exclude_types = []) { scope :browserable, ->(exclude_types = []) {
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types + [:follow_request]) types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types + [:follow_request])
where(activity_type: types) where(activity_type: types)
@ -68,6 +66,10 @@ class Notification < ApplicationRecord
end end
class << self class << self
def cache_ids
select(:id, :updated_at, :activity_type, :activity_id)
end
def reload_stale_associations!(cached_items) def reload_stale_associations!(cached_items)
account_ids = (cached_items.map(&:from_account_id) + cached_items.map { |item| item.target_status&.account_id }.compact).uniq account_ids = (cached_items.map(&:from_account_id) + cached_items.map { |item| item.target_status&.account_id }.compact).uniq

View File

@ -26,8 +26,6 @@
# #
class Status < ApplicationRecord class Status < ApplicationRecord
self.cache_versioning = false
include Paginable include Paginable
include Streamable include Streamable
include Cacheable include Cacheable

View File

@ -15,13 +15,13 @@ class AfterBlockDomainFromAccountService < BaseService
private private
def reject_existing_followers! def reject_existing_followers!
@account.passive_relationships.where(account: Account.where(domain: @domain)).includes(:account).find_each do |follow| @account.passive_relationships.where(account: Account.where(domain: @domain)).includes(:account).reorder(nil).find_each do |follow|
reject_follow!(follow) reject_follow!(follow)
end end
end end
def reject_pending_follow_requests! def reject_pending_follow_requests!
FollowRequest.where(target_account: @account).where(account: Account.where(domain: @domain)).includes(:account).find_each do |follow_request| FollowRequest.where(target_account: @account).where(account: Account.where(domain: @domain)).includes(:account).reorder(nil).find_each do |follow_request|
reject_follow!(follow_request) reject_follow!(follow_request)
end end
end end

View File

@ -18,7 +18,7 @@ class BackupService < BaseService
def build_json! def build_json!
@collection = serialize(collection_presenter, ActivityPub::CollectionSerializer) @collection = serialize(collection_presenter, ActivityPub::CollectionSerializer)
account.statuses.with_includes.find_in_batches do |statuses| account.statuses.with_includes.reorder(nil).find_in_batches do |statuses|
statuses.each do |status| statuses.each do |status|
item = serialize(status, ActivityPub::ActivitySerializer) item = serialize(status, ActivityPub::ActivitySerializer)
item.delete(:'@context') item.delete(:'@context')
@ -60,7 +60,7 @@ class BackupService < BaseService
end end
def dump_media_attachments!(tar) def dump_media_attachments!(tar)
MediaAttachment.attached.where(account: account).find_in_batches do |media_attachments| MediaAttachment.attached.where(account: account).reorder(nil).find_in_batches do |media_attachments|
media_attachments.each do |m| media_attachments.each do |m|
download_to_tar(tar, m.file, m.file.path) download_to_tar(tar, m.file, m.file.path)
end end

View File

@ -43,14 +43,14 @@ class BlockDomainService < BaseService
end end
def suspend_accounts! def suspend_accounts!
blocked_domain_accounts.where(suspended: false).find_each do |account| blocked_domain_accounts.where(suspended: false).reorder(nil).find_each do |account|
UnsubscribeService.new.call(account) if account.subscribed? UnsubscribeService.new.call(account) if account.subscribed?
SuspendAccountService.new.call(account) SuspendAccountService.new.call(account)
end end
end end
def clear_account_images! def clear_account_images!
blocked_domain_accounts.find_each do |account| blocked_domain_accounts.reorder(nil).find_each do |account|
account.avatar.destroy if account.avatar.exists? account.avatar.destroy if account.avatar.exists?
account.header.destroy if account.header.exists? account.header.destroy if account.header.exists?
account.save account.save
@ -58,7 +58,7 @@ class BlockDomainService < BaseService
end end
def clear_account_attachments! def clear_account_attachments!
media_from_blocked_domain.find_each do |attachment| media_from_blocked_domain.reorder(nil).find_each do |attachment|
@affected_status_ids << attachment.status_id if attachment.status_id.present? @affected_status_ids << attachment.status_id if attachment.status_id.present?
attachment.file.destroy if attachment.file.exists? attachment.file.destroy if attachment.file.exists?

View File

@ -43,13 +43,13 @@ class RemoveStatusService < BaseService
end end
def remove_from_followers def remove_from_followers
@account.followers_for_local_distribution.find_each do |follower| @account.followers_for_local_distribution.reorder(nil).find_each do |follower|
FeedManager.instance.unpush_from_home(follower, @status) FeedManager.instance.unpush_from_home(follower, @status)
end end
end end
def remove_from_lists def remove_from_lists
@account.lists_for_local_distribution.select(:id, :account_id).find_each do |list| @account.lists_for_local_distribution.select(:id, :account_id).reorder(nil).find_each do |list|
FeedManager.instance.unpush_from_list(list, @status) FeedManager.instance.unpush_from_list(list, @status)
end end
end end

View File

@ -23,9 +23,7 @@ class SuspendAccountService < BaseService
def purge_content! def purge_content!
if @account.local? if @account.local?
ActivityPub::RawDistributionWorker.perform_async(delete_actor_json, @account.id) ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url|
ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url|
[delete_actor_json, @account.id, inbox_url] [delete_actor_json, @account.id, inbox_url]
end end
end end
@ -75,4 +73,8 @@ class SuspendAccountService < BaseService
@delete_actor_json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account)) @delete_actor_json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
end end
def delivery_inboxes
Account.inboxes + Relay.enabled.pluck(:inbox_url)
end
end end

View File

@ -8,7 +8,7 @@ class Maintenance::UncacheMediaWorker
def perform(media_attachment_id) def perform(media_attachment_id)
media = MediaAttachment.find(media_attachment_id) media = MediaAttachment.find(media_attachment_id)
return unless media.file.exists? return if media.file.blank?
media.file.destroy media.file.destroy
media.save media.save

View File

@ -9,7 +9,7 @@ class RefollowWorker
target_account = Account.find(target_account_id) target_account = Account.find(target_account_id)
return unless target_account.protocol == :activitypub return unless target_account.protocol == :activitypub
target_account.followers.where(domain: nil).find_each do |follower| target_account.followers.where(domain: nil).reorder(nil).find_each do |follower|
# Locally unfollow remote account # Locally unfollow remote account
follower.unfollow!(target_account) follower.unfollow!(target_account)

View File

@ -3,8 +3,10 @@
class Scheduler::BackupCleanupScheduler class Scheduler::BackupCleanupScheduler
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options unique: :until_executed
def perform def perform
old_backups.find_each(&:destroy!) old_backups.reorder(nil).find_each(&:destroy!)
end end
private private

View File

@ -3,6 +3,8 @@
class Scheduler::DoorkeeperCleanupScheduler class Scheduler::DoorkeeperCleanupScheduler
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options unique: :until_executed
def perform def perform
Doorkeeper::AccessToken.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all Doorkeeper::AccessToken.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all
Doorkeeper::AccessGrant.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all Doorkeeper::AccessGrant.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all

View File

@ -3,8 +3,10 @@
class Scheduler::EmailScheduler class Scheduler::EmailScheduler
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options unique: :until_executed
def perform def perform
eligible_users.find_each do |user| eligible_users.reorder(nil).find_each do |user|
next unless user.allows_digest_emails? next unless user.allows_digest_emails?
DigestMailerWorker.perform_async(user.id) DigestMailerWorker.perform_async(user.id)
end end

View File

@ -3,6 +3,8 @@
class Scheduler::FeedCleanupScheduler class Scheduler::FeedCleanupScheduler
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options unique: :until_executed
def perform def perform
clean_home_feeds! clean_home_feeds!
clean_list_feeds! clean_list_feeds!

View File

@ -5,6 +5,8 @@ class Scheduler::IpCleanupScheduler
RETENTION_PERIOD = 1.year RETENTION_PERIOD = 1.year
sidekiq_options unique: :until_executed
def perform def perform
time_ago = RETENTION_PERIOD.ago time_ago = RETENTION_PERIOD.ago
SessionActivation.where('updated_at < ?', time_ago).destroy_all SessionActivation.where('updated_at < ?', time_ago).destroy_all

View File

@ -3,6 +3,8 @@
class Scheduler::MediaCleanupScheduler class Scheduler::MediaCleanupScheduler
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options unique: :until_executed
def perform def perform
unattached_media.find_each(&:destroy) unattached_media.find_each(&:destroy)
end end

View File

@ -3,6 +3,8 @@
class Scheduler::SubscriptionsCleanupScheduler class Scheduler::SubscriptionsCleanupScheduler
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options unique: :until_executed
def perform def perform
Subscription.expired.in_batches.delete_all Subscription.expired.in_batches.delete_all
end end

View File

@ -3,6 +3,8 @@
class Scheduler::SubscriptionsScheduler class Scheduler::SubscriptionsScheduler
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options unique: :until_executed
def perform def perform
Pubsubhubbub::SubscribeWorker.push_bulk(expiring_accounts.pluck(:id)) Pubsubhubbub::SubscribeWorker.push_bulk(expiring_accounts.pluck(:id))
end end

View File

@ -3,8 +3,10 @@
class Scheduler::UserCleanupScheduler class Scheduler::UserCleanupScheduler
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options unique: :until_executed
def perform def perform
User.where('confirmed_at is NULL AND confirmation_sent_at <= ?', 2.days.ago).find_in_batches do |batch| User.where('confirmed_at is NULL AND confirmation_sent_at <= ?', 2.days.ago).reorder(nil).find_in_batches do |batch|
Account.where(id: batch.map(&:account_id)).delete_all Account.where(id: batch.map(&:account_id)).delete_all
User.where(id: batch.map(&:id)).delete_all User.where(id: batch.map(&:id)).delete_all
end end

View File

@ -74,14 +74,10 @@ elsif ENV['SWIFT_ENABLED'] == 'true'
fog_public: true fog_public: true
) )
else else
require 'fog/local'
Paperclip::Attachment.default_options.merge!( Paperclip::Attachment.default_options.merge!(
fog_credentials: { storage: :filesystem,
provider: 'Local', use_timestamp: true,
local_root: ENV.fetch('PAPERCLIP_ROOT_PATH') { Rails.root.join('public', 'system') }, path: (ENV['PAPERCLIP_ROOT_PATH'] || ':rails_root/public/system') + '/:class/:attachment/:id_partition/:style/:filename',
}, url: (ENV['PAPERCLIP_ROOT_URL'] || '/system') + '/:class/:attachment/:id_partition/:style/:filename',
fog_directory: '',
fog_host: ENV.fetch('PAPERCLIP_ROOT_URL') { '/system' }
) )
end end

View File

@ -725,7 +725,7 @@ fi:
tip_local_timeline: Paikallinen aikajana näyttää instanssin %{instance} käyttäjien julkaisut. He ovat naapureitasi! tip_local_timeline: Paikallinen aikajana näyttää instanssin %{instance} käyttäjien julkaisut. He ovat naapureitasi!
tip_mobile_webapp: Jos voit lisätä Mastodonin mobiiliselaimen kautta aloitusnäytöllesi, voit vastaanottaa push-ilmoituksia. Toiminta vastaa monin tavoin tavanomaista sovellusta! tip_mobile_webapp: Jos voit lisätä Mastodonin mobiiliselaimen kautta aloitusnäytöllesi, voit vastaanottaa push-ilmoituksia. Toiminta vastaa monin tavoin tavanomaista sovellusta!
tips: Vinkkejä tips: Vinkkejä
title: Tervetuloa mukaan, %name}! title: Tervetuloa mukaan, %{name}!
users: users:
invalid_email: Virheellinen sähköpostiosoite invalid_email: Virheellinen sähköpostiosoite
invalid_otp_token: Virheellinen kaksivaiheisen todentamisen koodi invalid_otp_token: Virheellinen kaksivaiheisen todentamisen koodi

View File

@ -6,6 +6,7 @@ pl:
about_this: O tej instancji about_this: O tej instancji
administered_by: 'Administrowana przez:' administered_by: 'Administrowana przez:'
api: API api: API
apps: Aplikacje
closed_registrations: Rejestracja na tej instancji jest obecnie zamknięta. Możesz jednak zarejestrować się na innej instancji, uzyskując dostęp do tej samej sieci. closed_registrations: Rejestracja na tej instancji jest obecnie zamknięta. Możesz jednak zarejestrować się na innej instancji, uzyskując dostęp do tej samej sieci.
contact: Kontakt contact: Kontakt
contact_missing: Nie ustawiono contact_missing: Nie ustawiono
@ -281,6 +282,7 @@ pl:
search: Szukaj search: Szukaj
title: Znane instancje title: Znane instancje
invites: invites:
deactivate_all: Unieważnij wszystkie
filter: filter:
all: Wszystkie all: Wszystkie
available: Dostępne available: Dostępne
@ -663,11 +665,14 @@ pl:
publishing: Publikowanie publishing: Publikowanie
web: Sieć web: Sieć
remote_follow: remote_follow:
acct: Podaj swój adres (nazwa@domena), z którego chcesz śledzić acct: Podaj swój adres (nazwa@domena), z którego chcesz wykonać działanie
missing_resource: Nie udało się znaleźć adresu przekierowania z Twojej domeny missing_resource: Nie udało się znaleźć adresu przekierowania z Twojej domeny
no_account_html: Nie masz konta? Możesz <a href='%{sign_up_path}' target='_blank'>zarejestrować się tutaj</a> no_account_html: Nie masz konta? Możesz <a href='%{sign_up_path}' target='_blank'>zarejestrować się tutaj</a>
proceed: Śledź proceed: Śledź
prompt: 'Zamierzasz śledzić:' prompt: 'Zamierzasz śledzić:'
remote_interaction:
proceed: Przejdź do interakcji
prompt: 'Chcesz dokonać interakcji z tym wpisem:'
remote_unfollow: remote_unfollow:
error: Błąd error: Błąd
title: Tytuł title: Tytuł
@ -756,6 +761,7 @@ pl:
private: Nie możesz przypiąć niepublicznego wpisu private: Nie możesz przypiąć niepublicznego wpisu
reblog: Nie możesz przypiąć podbicia wpisu reblog: Nie możesz przypiąć podbicia wpisu
show_more: Pokaż więcej show_more: Pokaż więcej
sign_in_to_participate: Zaloguj się, aby udzielić się w tej konwersacji
title: '%{name}: "%{quote}"' title: '%{name}: "%{quote}"'
visibilities: visibilities:
private: Tylko dla śledzących private: Tylko dla śledzących

View File

@ -267,18 +267,19 @@ Rails.application.routes.draw do
get '/search', to: 'search#index', as: :search get '/search', to: 'search#index', as: :search
resources :follows, only: [:create] resources :follows, only: [:create]
resources :media, only: [:create, :update] resources :media, only: [:create, :update]
resources :blocks, only: [:index] resources :blocks, only: [:index]
resources :mutes, only: [:index] do resources :mutes, only: [:index] do
collection do collection do
get 'details' get 'details'
end end
end end
resources :favourites, only: [:index] resources :favourites, only: [:index]
resources :bookmarks, only: [:index] resources :bookmarks, only: [:index]
resources :reports, only: [:index, :create] resources :reports, only: [:index, :create]
resources :filters, only: [:index, :create, :show, :update, :destroy] resources :filters, only: [:index, :create, :show, :update, :destroy]
resources :endorsements, only: [:index]
namespace :apps do namespace :apps do
get :verify_credentials, to: 'credentials#show' get :verify_credentials, to: 'credentials#show'

View File

@ -503,7 +503,7 @@ namespace :mastodon do
desc 'Remove media attachments attributed to silenced accounts' desc 'Remove media attachments attributed to silenced accounts'
task remove_silenced: :environment do task remove_silenced: :environment do
nb_media_attachments = 0 nb_media_attachments = 0
MediaAttachment.where(account: Account.silenced).select(:id).find_in_batches do |media_attachments| MediaAttachment.where(account: Account.silenced).select(:id).reorder(nil).find_in_batches do |media_attachments|
nb_media_attachments += media_attachments.length nb_media_attachments += media_attachments.length
Maintenance::DestroyMediaWorker.push_bulk(media_attachments.map(&:id)) Maintenance::DestroyMediaWorker.push_bulk(media_attachments.map(&:id))
end end
@ -515,7 +515,7 @@ namespace :mastodon do
time_ago = ENV.fetch('NUM_DAYS') { 7 }.to_i.days.ago time_ago = ENV.fetch('NUM_DAYS') { 7 }.to_i.days.ago
nb_media_attachments = 0 nb_media_attachments = 0
MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).select(:id).find_in_batches do |media_attachments| MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).select(:id).reorder(nil).find_in_batches do |media_attachments|
nb_media_attachments += media_attachments.length nb_media_attachments += media_attachments.length
Maintenance::UncacheMediaWorker.push_bulk(media_attachments.map(&:id)) Maintenance::UncacheMediaWorker.push_bulk(media_attachments.map(&:id))
end end
@ -535,7 +535,7 @@ namespace :mastodon do
accounts = accounts.where(domain: ENV['DOMAIN']) if ENV['DOMAIN'].present? accounts = accounts.where(domain: ENV['DOMAIN']) if ENV['DOMAIN'].present?
nb_accounts = 0 nb_accounts = 0
accounts.select(:id).find_in_batches do |accounts_batch| accounts.select(:id).reorder(nil).find_in_batches do |accounts_batch|
nb_accounts += accounts_batch.length nb_accounts += accounts_batch.length
Maintenance::RedownloadAccountMediaWorker.push_bulk(accounts_batch.map(&:id)) Maintenance::RedownloadAccountMediaWorker.push_bulk(accounts_batch.map(&:id))
end end
@ -570,7 +570,7 @@ namespace :mastodon do
desc 'Generates home timelines for users who logged in in the past two weeks' desc 'Generates home timelines for users who logged in in the past two weeks'
task build: :environment do task build: :environment do
User.active.select(:id, :account_id).find_in_batches do |users| User.active.select(:id, :account_id).reorder(nil).find_in_batches do |users|
RegenerationWorker.push_bulk(users.map(&:account_id)) RegenerationWorker.push_bulk(users.map(&:account_id))
end end
end end

View File

@ -2,6 +2,11 @@ require 'rails_helper'
RSpec.describe SuspendAccountService, type: :service do RSpec.describe SuspendAccountService, type: :service do
describe '#call' do describe '#call' do
before do
stub_request(:post, "https://alice.com/inbox").to_return(status: 201)
stub_request(:post, "https://bob.com/inbox").to_return(status: 201)
end
subject do subject do
-> { described_class.new.call(account) } -> { described_class.new.call(account) }
end end
@ -14,6 +19,8 @@ RSpec.describe SuspendAccountService, type: :service do
let!(:active_relationship) { Fabricate(:follow, account: account) } let!(:active_relationship) { Fabricate(:follow, account: account) }
let!(:passive_relationship) { Fabricate(:follow, target_account: account) } let!(:passive_relationship) { Fabricate(:follow, target_account: account) }
let!(:subscription) { Fabricate(:subscription, account: account) } let!(:subscription) { Fabricate(:subscription, account: account) }
let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', protocol: :activitypub) }
let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
it 'deletes associated records' do it 'deletes associated records' do
is_expected.to change { is_expected.to change {
@ -29,5 +36,11 @@ RSpec.describe SuspendAccountService, type: :service do
].map(&:count) ].map(&:count)
}.from([1, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0]) }.from([1, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0])
end end
it 'sends a delete actor activity to all known inboxes' do
subject.call
expect(a_request(:post, "https://alice.com/inbox")).to have_been_made.once
expect(a_request(:post, "https://bob.com/inbox")).to have_been_made.once
end
end end
end end