Merge branch 'master' into glitch-soc/master

Conflicts:
	config/routes.rb

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

View File

@ -43,7 +43,7 @@ class AccountsController < ApplicationController
format.json do
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)
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
def show
cached = Rails.cache.read(@status.cache_key)
@status = cached unless cached.nil?
@status = cache_collection([@status], Status).first
render json: @status, serializer: REST::StatusSerializer
end

View File

@ -178,12 +178,8 @@ class ApplicationController < ActionController::Base
return raw unless klass.respond_to?(:with_includes)
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
uncached_ids = []
cached_keys_with_value = Rails.cache.read_multi(*raw.map(&:cache_key))
raw.each do |item|
uncached_ids << item.id unless cached_keys_with_value.key?(item.cache_key)
end
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
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.each_value do |item|
Rails.cache.write(item.cache_key, item)
Rails.cache.write(item, item)
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
def respond_with_error(code)
@ -211,7 +207,6 @@ class ApplicationController < ActionController::Base
def render_cached_json(cache_key, **options)
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
content_type = options.delete(:content_type) || 'application/json'

View File

@ -9,7 +9,7 @@ class EmojisController < ApplicationController
format.json do
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)
end
end

View File

@ -34,7 +34,7 @@ class StatusesController < ApplicationController
format.json do
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)
end
end
@ -44,7 +44,7 @@ class StatusesController < ApplicationController
def activity
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)
end
end

View File

@ -55,7 +55,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
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]);
}

View File

@ -137,7 +137,7 @@ class DropdownMenu extends React.PureComponent {
// It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by
// 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 }} />
<ul>

View File

@ -28,6 +28,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
style: PropTypes.object,
items: PropTypes.array.isRequired,
value: PropTypes.string.isRequired,
placement: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
@ -119,7 +120,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
render () {
const { mounted } = this.state;
const { style, items, value } = this.props;
const { style, items, placement, value } = this.props;
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 }) }}>
@ -127,7 +128,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
// It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by
// 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 => (
<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'>
@ -226,7 +227,7 @@ export default class PrivacyDropdown extends React.PureComponent {
const valueOption = this.options.find(item => item.value === value);
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 })}>
<IconButton
className='privacy-dropdown__value-icon'
@ -247,6 +248,7 @@ export default class PrivacyDropdown extends React.PureComponent {
value={value}
onClose={this.handleClose}
onChange={this.handleChange}
placement={placement}
/>
</Overlay>
</div>

View File

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

View File

@ -1,5 +1,6 @@
import { createSelector } from 'reselect';
import { List as ImmutableList } from 'immutable';
import { me } from '../initial_state';
const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
@ -83,7 +84,7 @@ export const makeGetStatus = () => {
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'));
return statusBase.withMutations(map => {

View File

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

View File

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

View File

@ -11,6 +11,7 @@
#
class AccountPin < ApplicationRecord
include Paginable
include RelationshipCacheable
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)
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)
log_action :update, status
end
@ -35,7 +35,7 @@ class Form::StatusBatch
end
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)
log_action :destroy, status
end

View File

@ -39,8 +39,6 @@ class Notification < ApplicationRecord
validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
scope :cache_ids, -> { select(:id, :updated_at, :activity_type, :activity_id) }
scope :browserable, ->(exclude_types = []) {
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types + [:follow_request])
where(activity_type: types)
@ -68,6 +66,10 @@ class Notification < ApplicationRecord
end
class << self
def cache_ids
select(:id, :updated_at, :activity_type, :activity_id)
end
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

View File

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

View File

@ -15,13 +15,13 @@ class AfterBlockDomainFromAccountService < BaseService
private
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)
end
end
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)
end
end

View File

@ -18,7 +18,7 @@ class BackupService < BaseService
def build_json!
@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|
item = serialize(status, ActivityPub::ActivitySerializer)
item.delete(:'@context')
@ -60,7 +60,7 @@ class BackupService < BaseService
end
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|
download_to_tar(tar, m.file, m.file.path)
end

View File

@ -43,14 +43,14 @@ class BlockDomainService < BaseService
end
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?
SuspendAccountService.new.call(account)
end
end
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.header.destroy if account.header.exists?
account.save
@ -58,7 +58,7 @@ class BlockDomainService < BaseService
end
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?
attachment.file.destroy if attachment.file.exists?

View File

@ -43,13 +43,13 @@ class RemoveStatusService < BaseService
end
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)
end
end
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)
end
end

View File

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

View File

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

View File

@ -9,7 +9,7 @@ class RefollowWorker
target_account = Account.find(target_account_id)
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
follower.unfollow!(target_account)

View File

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

View File

@ -3,6 +3,8 @@
class Scheduler::DoorkeeperCleanupScheduler
include Sidekiq::Worker
sidekiq_options unique: :until_executed
def perform
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,10 @@
class Scheduler::UserCleanupScheduler
include Sidekiq::Worker
sidekiq_options unique: :until_executed
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
User.where(id: batch.map(&:id)).delete_all
end