Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
	app/models/status.rb
	db/migrate/20180528141303_fix_accounts_unique_index.rb
	db/schema.rb

Resolved by taking upstream changes (no real conflicts, just glitch-soc
specific code too close to actual changes).
This commit is contained in:
Thibaut Girka 2018-08-17 17:43:54 +02:00
commit 280d7b1df8
43 changed files with 353 additions and 91 deletions

View File

@ -165,6 +165,7 @@ STREAMING_CLUSTER_NUM=1
# LDAP_BIND_DN= # LDAP_BIND_DN=
# LDAP_PASSWORD= # LDAP_PASSWORD=
# LDAP_UID=cn # LDAP_UID=cn
# LDAP_SEARCH_FILTER="%{uid}=%{email}"
# PAM authentication (optional) # PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable # PAM authentication uses for the email generation the "email" pam variable

View File

@ -41,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.1)
railties (>= 4.2) railties (>= 4.2)
dotenv (2.2.2) dotenv (2.2.2)
dotenv-rails (2.2.2) dotenv-rails (2.2.2)
@ -672,7 +672,7 @@ 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)

View File

@ -28,6 +28,10 @@ module Admin
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button)) @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_statuses_path(@account.id, current_params)
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
redirect_to admin_account_statuses_path(@account.id, current_params) redirect_to admin_account_statuses_path(@account.id, current_params)
end end

View File

@ -34,7 +34,11 @@ module Admin::ActionLogsHelper
link_to attributes['domain'], "https://#{attributes['domain']}" link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status' when 'Status'
tmp_status = Status.new(attributes) tmp_status = Status.new(attributes)
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", TagManager.instance.url_for(tmp_status) 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
end end
end end

View File

@ -1,9 +1,6 @@
// This file will be loaded on admin pages, regardless of theme. // This file will be loaded on admin pages, regardless of theme.
import { delegate } from 'rails-ujs'; import { delegate } from 'rails-ujs';
import { start } from '../mastodon/common';
start();
function handleDeleteStatus(event) { function handleDeleteStatus(event) {
const [data] = event.detail; const [data] = event.detail;

View File

@ -32,6 +32,16 @@ const messages = defineMessages({
embed: { id: 'status.embed', defaultMessage: 'Embed' }, embed: { id: 'status.embed', defaultMessage: 'Embed' },
}); });
const obfuscatedCount = count => {
if (count < 0) {
return 0;
} else if (count <= 1) {
return count;
} else {
return '1+';
}
};
@injectIntl @injectIntl
export default class StatusActionBar extends ImmutablePureComponent { export default class StatusActionBar extends ImmutablePureComponent {
@ -194,7 +204,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
return ( return (
<div className='status__action-bar'> <div className='status__action-bar'>
<IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /> <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{shareButton} {shareButton}

View File

@ -147,17 +147,17 @@ export default class ActionBar extends React.PureComponent {
<div className='account__action-bar'> <div className='account__action-bar'>
<div className='account__action-bar-links'> <div className='account__action-bar-links'>
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<FormattedMessage id='account.posts' defaultMessage='Toots' /> <FormattedMessage id='account.posts' defaultMessage='Toots' />
<strong>{shortNumberFormat(account.get('statuses_count'))}</strong> <strong>{shortNumberFormat(account.get('statuses_count'))}</strong>
</Link> </Link>
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}> <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<FormattedMessage id='account.follows' defaultMessage='Follows' /> <FormattedMessage id='account.follows' defaultMessage='Follows' />
<strong>{shortNumberFormat(account.get('following_count'))}</strong> <strong>{shortNumberFormat(account.get('following_count'))}</strong>
</Link> </Link>
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}> <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<FormattedMessage id='account.followers' defaultMessage='Followers' /> <FormattedMessage id='account.followers' defaultMessage='Followers' />
<strong>{shortNumberFormat(account.get('followers_count'))}</strong> <strong>{shortNumberFormat(account.get('followers_count'))}</strong>
</Link> </Link>

View File

@ -355,7 +355,9 @@ export default class Status extends ImmutablePureComponent {
if (status && ancestorsIds && ancestorsIds.size > 0) { if (status && ancestorsIds && ancestorsIds.size > 0) {
const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1]; const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1];
window.requestAnimationFrame(() => {
element.scrollIntoView(true); element.scrollIntoView(true);
});
this._scrolledIntoView = true; this._scrolledIntoView = true;
} }
} }

View File

@ -158,6 +158,9 @@ export default class Video extends React.PureComponent {
this.setState({ dragging: true }); this.setState({ dragging: true });
this.video.pause(); this.video.pause();
this.handleMouseMove(e); this.handleMouseMove(e);
e.preventDefault();
e.stopPropagation();
} }
handleMouseUp = () => { handleMouseUp = () => {
@ -174,8 +177,10 @@ export default class Video extends React.PureComponent {
const { x } = getPointerPosition(this.seek, e); const { x } = getPointerPosition(this.seek, e);
const currentTime = Math.floor(this.video.duration * x); const currentTime = Math.floor(this.video.duration * x);
if (!isNaN(currentTime)) {
this.video.currentTime = currentTime; this.video.currentTime = currentTime;
this.setState({ currentTime }); this.setState({ currentTime });
}
}, 60); }, 60);
togglePlay = () => { togglePlay = () => {
@ -281,6 +286,15 @@ export default class Video extends React.PureComponent {
playerStyle.height = height; playerStyle.height = height;
} }
let preload;
if (startTime || fullscreen || dragging) {
preload = 'auto';
} else if (detailed) {
preload = 'metadata';
} else {
preload = 'none';
}
return ( return (
<div <div
role='menuitem' role='menuitem'
@ -296,7 +310,7 @@ export default class Video extends React.PureComponent {
ref={this.setVideoRef} ref={this.setVideoRef}
src={src} src={src}
poster={preview} poster={preview}
preload={startTime ? 'auto' : 'none'} preload={preload}
loop loop
role='button' role='button'
tabIndex='0' tabIndex='0'

View File

@ -921,15 +921,31 @@
align-items: center; align-items: center;
display: flex; display: flex;
margin-top: 8px; margin-top: 8px;
&__counter {
display: inline-flex;
margin-right: 11px;
align-items: center;
.status__action-bar-button {
margin-right: 4px;
}
&__label {
display: inline-block;
width: 14px;
font-size: 12px;
font-weight: 500;
color: $action-button-color;
}
}
} }
.status__action-bar-button { .status__action-bar-button {
float: left;
margin-right: 18px; margin-right: 18px;
} }
.status__action-bar-dropdown { .status__action-bar-dropdown {
float: left;
height: 23.15px; height: 23.15px;
width: 23.15px; width: 23.15px;
} }

View File

@ -1,9 +1,3 @@
@keyframes Swag {
0% { background-position: 0% 0%; }
50% { background-position: 100% 0%; }
100% { background-position: 200% 0%; }
}
.table { .table {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
@ -191,14 +185,12 @@ a.table-action-link {
.status__content { .status__content {
padding-top: 0; padding-top: 0;
summary {
display: list-item;
}
strong { strong {
font-weight: 700; font-weight: 700;
background: linear-gradient(to right, orange , yellow, green, cyan, blue, violet,orange , yellow, green, cyan, blue, violet);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: Swag 2s linear 0s infinite;
} }
} }
} }

View File

@ -13,7 +13,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
# Fast-forward repeat follow requests # Fast-forward repeat follow requests
if @account.following?(target_account) if @account.following?(target_account)
AuthorizeFollowService.new.call(@account, target_account, skip_follow_request: true) AuthorizeFollowService.new.call(@account, target_account, skip_follow_request: true, follow_request_uri: @json['id'])
return return
end end

View File

@ -5,6 +5,8 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
case @object['type'] case @object['type']
when 'Announce' when 'Announce'
undo_announce undo_announce
when 'Accept'
undo_accept
when 'Follow' when 'Follow'
undo_follow undo_follow
when 'Like' when 'Like'
@ -27,6 +29,10 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
end end
end end
def undo_accept
::Follow.find_by(target_account: @account, uri: target_uri)&.revoke_request!
end
def undo_follow def undo_follow
target_account = account_from_uri(target_uri) target_account = account_from_uri(target_uri)

View File

@ -24,8 +24,16 @@ class Export
account.media_attachments.sum(:file_file_size) account.media_attachments.sum(:file_file_size)
end end
def total_statuses
account.statuses_count
end
def total_follows def total_follows
account.following.count account.following_count
end
def total_followers
account.followers_count
end end
def total_blocks def total_blocks

View File

@ -32,20 +32,11 @@ class Favourite < ApplicationRecord
private private
def increment_cache_counters def increment_cache_counters
if association(:status).loaded? status.increment_count!(:favourites_count)
status.update_attribute(:favourites_count, status.favourites_count + 1)
else
Status.where(id: status_id).update_all('favourites_count = COALESCE(favourites_count, 0) + 1')
end
end end
def decrement_cache_counters def decrement_cache_counters
return if association(:status).loaded? && (status.marked_for_destruction? || status.marked_for_mass_destruction?) return if association(:status).loaded? && (status.marked_for_destruction? || status.marked_for_mass_destruction?)
status.decrement_count!(:favourites_count)
if association(:status).loaded?
status.update_attribute(:favourites_count, [status.favourites_count - 1, 0].max)
else
Status.where(id: status_id).update_all('favourites_count = GREATEST(COALESCE(favourites_count, 0) - 1, 0)')
end
end end
end end

View File

@ -32,6 +32,11 @@ class Follow < ApplicationRecord
false # Force uri_for to use uri attribute false # Force uri_for to use uri attribute
end end
def revoke_request!
FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, uri: uri)
destroy!
end
before_validation :set_uri, only: :create before_validation :set_uri, only: :create
after_destroy :remove_endorsements after_destroy :remove_endorsements

View File

@ -15,8 +15,6 @@
# visibility :integer default("public"), not null # visibility :integer default("public"), not null
# spoiler_text :text default(""), not null # spoiler_text :text default(""), not null
# reply :boolean default(FALSE), not null # reply :boolean default(FALSE), not null
# favourites_count :integer default(0), not null
# reblogs_count :integer default(0), not null
# language :string # language :string
# conversation_id :bigint(8) # conversation_id :bigint(8)
# local :boolean # local :boolean
@ -28,6 +26,8 @@
# #
class Status < ApplicationRecord class Status < ApplicationRecord
self.cache_versioning = false
include Paginable include Paginable
include Streamable include Streamable
include Cacheable include Cacheable
@ -62,6 +62,7 @@ class Status < ApplicationRecord
has_one :notification, as: :activity, dependent: :destroy has_one :notification, as: :activity, dependent: :destroy
has_one :stream_entry, as: :activity, inverse_of: :status has_one :stream_entry, as: :activity, inverse_of: :status
has_one :status_stat, inverse_of: :status
validates :uri, uniqueness: true, presence: true, unless: :local? validates :uri, uniqueness: true, presence: true, unless: :local?
validates :text, presence: true, unless: -> { with_media? || reblog? } validates :text, presence: true, unless: -> { with_media? || reblog? }
@ -86,7 +87,25 @@ class Status < ApplicationRecord
scope :not_local_only, -> { where(local_only: [false, nil]) } scope :not_local_only, -> { where(local_only: [false, nil]) }
cache_associated :account, :application, :media_attachments, :conversation, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, :conversation, mentions: :account], thread: :account cache_associated :account,
:application,
:media_attachments,
:conversation,
:status_stat,
:tags,
:stream_entry,
mentions: :account,
reblog: [
:account,
:application,
:stream_entry,
:tags,
:media_attachments,
:conversation,
:status_stat,
mentions: :account,
],
thread: :account
delegate :domain, to: :account, prefix: true delegate :domain, to: :account, prefix: true
@ -180,6 +199,26 @@ class Status < ApplicationRecord
@marked_for_mass_destruction @marked_for_mass_destruction
end end
def replies_count
status_stat&.replies_count || 0
end
def reblogs_count
status_stat&.reblogs_count || 0
end
def favourites_count
status_stat&.favourites_count || 0
end
def increment_count!(key)
update_status_stat!(key => public_send(key) + 1)
end
def decrement_count!(key)
update_status_stat!(key => [public_send(key) - 1, 0].max)
end
after_create :increment_counter_caches after_create :increment_counter_caches
after_destroy :decrement_counter_caches after_destroy :decrement_counter_caches
@ -197,6 +236,10 @@ class Status < ApplicationRecord
before_validation :set_local before_validation :set_local
class << self class << self
def cache_ids
left_outer_joins(:status_stat).select('statuses.id, greatest(statuses.updated_at, status_stats.updated_at) AS updated_at')
end
def in_chosen_languages(account) def in_chosen_languages(account)
where(language: nil).or where(language: account.chosen_languages) where(language: nil).or where(language: account.chosen_languages)
end end
@ -372,6 +415,11 @@ class Status < ApplicationRecord
private private
def update_status_stat!(attrs)
record = status_stat || build_status_stat
record.update(attrs)
end
def store_uri def store_uri
update_attribute(:uri, ActivityPub::TagManager.instance.uri_for(self)) if uri.nil? update_attribute(:uri, ActivityPub::TagManager.instance.uri_for(self)) if uri.nil?
end end
@ -434,13 +482,8 @@ class Status < ApplicationRecord
Account.where(id: account_id).update_all('statuses_count = COALESCE(statuses_count, 0) + 1') Account.where(id: account_id).update_all('statuses_count = COALESCE(statuses_count, 0) + 1')
end end
return unless reblog? reblog.increment_count!(:reblogs_count) if reblog?
thread.increment_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
if association(:reblog).loaded?
reblog.update_attribute(:reblogs_count, reblog.reblogs_count + 1)
else
Status.where(id: reblog_of_id).update_all('reblogs_count = COALESCE(reblogs_count, 0) + 1')
end
end end
def decrement_counter_caches def decrement_counter_caches
@ -452,12 +495,7 @@ class Status < ApplicationRecord
Account.where(id: account_id).update_all('statuses_count = GREATEST(COALESCE(statuses_count, 0) - 1, 0)') Account.where(id: account_id).update_all('statuses_count = GREATEST(COALESCE(statuses_count, 0) - 1, 0)')
end end
return unless reblog? reblog.decrement_count!(:reblogs_count) if reblog?
thread.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
if association(:reblog).loaded?
reblog.update_attribute(:reblogs_count, [reblog.reblogs_count - 1, 0].max)
else
Status.where(id: reblog_of_id).update_all('reblogs_count = GREATEST(COALESCE(reblogs_count, 0) - 1, 0)')
end
end end
end end

17
app/models/status_stat.rb Normal file
View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: status_stats
#
# id :bigint(8) not null, primary key
# status_id :bigint(8) not null
# replies_count :bigint(8) default(0), not null
# reblogs_count :bigint(8) default(0), not null
# favourites_count :bigint(8) default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class StatusStat < ApplicationRecord
belongs_to :status, inverse_of: :status_stat
end

View File

@ -3,7 +3,8 @@
class REST::StatusSerializer < ActiveModel::Serializer class REST::StatusSerializer < ActiveModel::Serializer
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
:sensitive, :spoiler_text, :visibility, :language, :sensitive, :spoiler_text, :visibility, :language,
:uri, :content, :url, :reblogs_count, :favourites_count :uri, :content, :url, :replies_count, :reblogs_count,
:favourites_count
attribute :favourited, if: :current_user? attribute :favourited, if: :current_user?
attribute :reblogged, if: :current_user? attribute :reblogged, if: :current_user?

View File

@ -3,7 +3,7 @@
class AuthorizeFollowService < BaseService class AuthorizeFollowService < BaseService
def call(source_account, target_account, **options) def call(source_account, target_account, **options)
if options[:skip_follow_request] if options[:skip_follow_request]
follow_request = FollowRequest.new(account: source_account, target_account: target_account) follow_request = FollowRequest.new(account: source_account, target_account: target_account, uri: options[:follow_request_uri])
else else
follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
follow_request.authorize! follow_request.authorize!

View File

@ -2,11 +2,13 @@
class ResolveURLService < BaseService class ResolveURLService < BaseService
include JsonLdHelper include JsonLdHelper
include Authorization
attr_reader :url attr_reader :url
def call(url) def call(url, on_behalf_of: nil)
@url = url @url = url
@on_behalf_of = on_behalf_of
return process_local_url if local_url? return process_local_url if local_url?
@ -84,6 +86,10 @@ class ResolveURLService < BaseService
def check_local_status(status) def check_local_status(status)
return if status.nil? return if status.nil?
status if status.public_visibility? || status.unlisted_visibility? authorize_with @on_behalf_of, status, :show?
status
rescue Mastodon::NotPermittedError
# Do not disclose the existence of status the user is not authorized to see
nil
end end
end end

View File

@ -53,7 +53,7 @@ class SearchService < BaseService
end end
def url_resource def url_resource
@_url_resource ||= ResolveURLService.new.call(query) @_url_resource ||= ResolveURLService.new.call(query, on_behalf_of: @account)
end end
def url_resource_symbol def url_resource_symbol

View File

@ -14,17 +14,17 @@
.public-account-header__tabs__tabs .public-account-header__tabs__tabs
.details-counters .details-counters
.counter{ class: active_nav_class(short_account_url(account)) } .counter{ class: active_nav_class(short_account_url(account)) }
= link_to short_account_url(account), class: 'u-url u-uid' do = link_to short_account_url(account), class: 'u-url u-uid', title: number_with_delimiter(account.statuses_count) do
%span.counter-number= number_to_human account.statuses_count, strip_insignificant_zeros: true %span.counter-number= number_to_human account.statuses_count, strip_insignificant_zeros: true
%span.counter-label= t('accounts.posts') %span.counter-label= t('accounts.posts')
.counter{ class: active_nav_class(account_following_index_url(account)) } .counter{ class: active_nav_class(account_following_index_url(account)) }
= link_to account_following_index_url(account) do = link_to account_following_index_url(account), title: number_with_delimiter(account.following_count) do
%span.counter-number= number_to_human account.following_count, strip_insignificant_zeros: true %span.counter-number= number_to_human account.following_count, strip_insignificant_zeros: true
%span.counter-label= t('accounts.following') %span.counter-label= t('accounts.following')
.counter{ class: active_nav_class(account_followers_url(account)) } .counter{ class: active_nav_class(account_followers_url(account)) }
= link_to account_followers_url(account) do = link_to account_followers_url(account), title: number_with_delimiter(account.followers_count) do
%span.counter-number= number_to_human account.followers_count, strip_insignificant_zeros: true %span.counter-number= number_to_human account.followers_count, strip_insignificant_zeros: true
%span.counter-label= t('accounts.followers') %span.counter-label= t('accounts.followers')
.spacer .spacer

View File

@ -3,10 +3,12 @@
= f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id
.batch-table__row__content .batch-table__row__content
.status__content>< .status__content><
- unless status.proper.spoiler_text.blank? - if status.proper.spoiler_text.blank?
%p>< = Formatter.instance.format(status.proper, custom_emojify: true)
- else
%details<
%summary><
%strong> Content warning: #{Formatter.instance.format_spoiler(status.proper)} %strong> Content warning: #{Formatter.instance.format_spoiler(status.proper)}
= Formatter.instance.format(status.proper, custom_emojify: true) = Formatter.instance.format(status.proper, custom_emojify: true)
- unless status.proper.media_attachments.empty? - unless status.proper.media_attachments.empty?

View File

@ -8,17 +8,25 @@
%th= t('exports.storage') %th= t('exports.storage')
%td= number_to_human_size @export.total_storage %td= number_to_human_size @export.total_storage
%td %td
%tr
%th= t('accounts.statuses')
%td= number_with_delimiter @export.total_statuses
%td
%tr %tr
%th= t('exports.follows') %th= t('exports.follows')
%td= number_to_human @export.total_follows %td= number_with_delimiter @export.total_follows
%td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv) %td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv)
%tr
%th= t('accounts.followers')
%td= number_with_delimiter @export.total_followers
%td
%tr %tr
%th= t('exports.blocks') %th= t('exports.blocks')
%td= number_to_human @export.total_blocks %td= number_with_delimiter @export.total_blocks
%td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv) %td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv)
%tr %tr
%th= t('exports.mutes') %th= t('exports.mutes')
%td= number_to_human @export.total_mutes %td= number_with_delimiter @export.total_mutes
%td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv) %td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv)
%p.muted-hint= t('exports.archive_takeout.hint_html') %p.muted-hint= t('exports.archive_takeout.hint_html')

View File

@ -1,11 +1,14 @@
- content_for :page_title do - content_for :page_title do
= t('settings.import') = t('settings.import')
%p.hint= t('imports.preface')
= simple_form_for @import, url: settings_import_path do |f| = simple_form_for @import, url: settings_import_path do |f|
%p.hint= t('imports.preface')
.field-group
= f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' = f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
= f.input :data, wrapper: :with_label, hint: t('simple_form.hints.imports.data')
.field-group
= f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data')
.actions .actions
= f.button :button, t('imports.upload'), type: :submit = f.button :button, t('imports.upload'), type: :submit

View File

@ -59,6 +59,8 @@ module Devise
@@ldap_password = nil @@ldap_password = nil
mattr_accessor :ldap_tls_no_verify mattr_accessor :ldap_tls_no_verify
@@ldap_tls_no_verify = false @@ldap_tls_no_verify = false
mattr_accessor :ldap_search_filter
@@ldap_search_filter = nil
class Strategies::PamAuthenticatable class Strategies::PamAuthenticatable
def valid? def valid?
@ -362,5 +364,6 @@ Devise.setup do |config|
config.ldap_password = ENV.fetch('LDAP_PASSWORD') config.ldap_password = ENV.fetch('LDAP_PASSWORD')
config.ldap_uid = ENV.fetch('LDAP_UID', 'cn') config.ldap_uid = ENV.fetch('LDAP_UID', 'cn')
config.ldap_tls_no_verify = ENV['LDAP_TLS_NO_VERIFY'] == 'true' config.ldap_tls_no_verify = ENV['LDAP_TLS_NO_VERIFY'] == 'true'
config.ldap_search_filter = ENV.fetch('LDAP_SEARCH_FILTER', '%{uid}=%{email}')
end end
end end

View File

@ -184,6 +184,7 @@ en:
unsuspend_account: "%{name} unsuspended %{target}'s account" unsuspend_account: "%{name} unsuspended %{target}'s account"
update_custom_emoji: "%{name} updated emoji %{target}" update_custom_emoji: "%{name} updated emoji %{target}"
update_status: "%{name} updated status by %{target}" update_status: "%{name} updated status by %{target}"
deleted_status: "(deleted status)"
title: Audit log title: Audit log
custom_emojis: custom_emojis:
by_domain: Domain by_domain: Domain
@ -401,6 +402,7 @@ en:
media: media:
title: Media title: Media
no_media: No media no_media: No media
no_status_selected: No statuses were changed as none were selected
title: Account statuses title: Account statuses
with_media: With media with_media: With media
subscriptions: subscriptions:

View File

@ -1,5 +1,3 @@
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
class ChangeAccountIdNonnullableInLists < ActiveRecord::Migration[5.1] class ChangeAccountIdNonnullableInLists < ActiveRecord::Migration[5.1]
def change def change
change_column_null :lists, :account_id, false change_column_null :lists, :account_id, false

View File

@ -6,6 +6,10 @@ class FixAccountsUniqueIndex < ActiveRecord::Migration[5.2]
def local? def local?
domain.nil? domain.nil?
end end
def acct
local? ? username : "#{username}@#{domain}"
end
end end
disable_ddl_transaction! disable_ddl_transaction!

View File

@ -0,0 +1,12 @@
class CreateStatusStats < ActiveRecord::Migration[5.2]
def change
create_table :status_stats do |t|
t.belongs_to :status, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true }
t.bigint :replies_count, null: false, default: 0
t.bigint :reblogs_count, null: false, default: 0
t.bigint :favourites_count, null: false, default: 0
t.timestamps
end
end
end

View File

@ -0,0 +1,19 @@
class CopyStatusStats < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
safety_assured do
execute <<-SQL.squish
INSERT INTO status_stats (status_id, reblogs_count, favourites_count, created_at, updated_at)
SELECT id, reblogs_count, favourites_count, created_at, updated_at
FROM statuses
ON CONFLICT (status_id) DO UPDATE
SET reblogs_count = EXCLUDED.reblogs_count, favourites_count = EXCLUDED.favourites_count
SQL
end
end
def down
# Nothing
end
end

View File

@ -0,0 +1,23 @@
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
class AddConfidentialToDoorkeeperApplication < ActiveRecord::Migration[5.2]
include Mastodon::MigrationHelpers
disable_ddl_transaction!
def up
safety_assured do
add_column_with_default(
:oauth_applications,
:confidential,
:boolean,
allow_null: false,
default: true # maintaining backwards compatibility: require secrets
)
end
end
def down
remove_column :oauth_applications, :confidential
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
class CopyStatusStatsCleanup < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
safety_assured do
remove_column :statuses, :reblogs_count, :integer, default: 0, null: false
remove_column :statuses, :favourites_count, :integer, default: 0, null: false
end
end
end

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2018_08_13_160548) do ActiveRecord::Schema.define(version: 2018_08_14_171349) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -359,6 +359,7 @@ ActiveRecord::Schema.define(version: 2018_08_13_160548) do
t.string "website" t.string "website"
t.string "owner_type" t.string "owner_type"
t.bigint "owner_id" t.bigint "owner_id"
t.boolean "confidential", default: true, null: false
t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type" t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type"
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
end end
@ -466,6 +467,16 @@ ActiveRecord::Schema.define(version: 2018_08_13_160548) do
t.index ["account_id", "status_id"], name: "index_status_pins_on_account_id_and_status_id", unique: true t.index ["account_id", "status_id"], name: "index_status_pins_on_account_id_and_status_id", unique: true
end end
create_table "status_stats", force: :cascade do |t|
t.bigint "status_id", null: false
t.bigint "replies_count", default: 0, null: false
t.bigint "reblogs_count", default: 0, null: false
t.bigint "favourites_count", default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["status_id"], name: "index_status_stats_on_status_id", unique: true
end
create_table "statuses", id: :bigint, default: -> { "timestamp_id('statuses'::text)" }, force: :cascade do |t| create_table "statuses", id: :bigint, default: -> { "timestamp_id('statuses'::text)" }, force: :cascade do |t|
t.string "uri" t.string "uri"
t.text "text", default: "", null: false t.text "text", default: "", null: false
@ -478,8 +489,6 @@ ActiveRecord::Schema.define(version: 2018_08_13_160548) do
t.integer "visibility", default: 0, null: false t.integer "visibility", default: 0, null: false
t.text "spoiler_text", default: "", null: false t.text "spoiler_text", default: "", null: false
t.boolean "reply", default: false, null: false t.boolean "reply", default: false, null: false
t.integer "favourites_count", default: 0, null: false
t.integer "reblogs_count", default: 0, null: false
t.string "language" t.string "language"
t.bigint "conversation_id" t.bigint "conversation_id"
t.boolean "local" t.boolean "local"
@ -643,6 +652,7 @@ ActiveRecord::Schema.define(version: 2018_08_13_160548) do
add_foreign_key "session_activations", "users", name: "fk_e5fda67334", on_delete: :cascade add_foreign_key "session_activations", "users", name: "fk_e5fda67334", on_delete: :cascade
add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade
add_foreign_key "status_pins", "statuses", on_delete: :cascade add_foreign_key "status_pins", "statuses", on_delete: :cascade
add_foreign_key "status_stats", "statuses", on_delete: :cascade
add_foreign_key "statuses", "accounts", column: "in_reply_to_account_id", name: "fk_c7fa917661", on_delete: :nullify add_foreign_key "statuses", "accounts", column: "in_reply_to_account_id", name: "fk_c7fa917661", on_delete: :nullify
add_foreign_key "statuses", "accounts", name: "fk_9bda1543f7", on_delete: :cascade add_foreign_key "statuses", "accounts", name: "fk_9bda1543f7", on_delete: :cascade
add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify

View File

@ -24,7 +24,8 @@ module Devise
connect_timeout: 10 connect_timeout: 10
) )
if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: "(#{Devise.ldap_uid}=#{email})", password: password)) filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, email: email)
if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: password))
user = User.ldap_get_user(user_info.first) user = User.ldap_get_user(user_info.first)
success!(user) success!(user)
else else

View File

@ -0,0 +1,6 @@
Fabricator(:status_stat) do
status_id nil
replies_count ""
reblogs_count ""
favourites_count ""
end

View File

@ -52,6 +52,32 @@ RSpec.describe ActivityPub::Activity::Undo do
end end
end end
context 'with Accept' do
let(:recipient) { Fabricate(:account) }
let(:object_json) do
{
id: 'bar',
type: 'Accept',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: 'follow-to-revoke',
}
end
before do
recipient.follow!(sender, uri: 'follow-to-revoke')
end
it 'deletes follow from recipient to sender' do
subject.perform
expect(recipient.following?(sender)).to be false
end
it 'creates a follow request from recipient to sender' do
subject.perform
expect(recipient.requested?(sender)).to be true
end
end
context 'with Block' do context 'with Block' do
let(:recipient) { Fabricate(:account) } let(:recipient) { Fabricate(:account) }

View File

@ -48,17 +48,17 @@ describe Export do
describe 'total_follows' do describe 'total_follows' do
it 'returns the total number of the followed accounts' do it 'returns the total number of the followed accounts' do
target_accounts.each(&account.method(:follow!)) target_accounts.each(&account.method(:follow!))
expect(Export.new(account).total_follows).to eq 2 expect(Export.new(account.reload).total_follows).to eq 2
end end
it 'returns the total number of the blocked accounts' do it 'returns the total number of the blocked accounts' do
target_accounts.each(&account.method(:block!)) target_accounts.each(&account.method(:block!))
expect(Export.new(account).total_blocks).to eq 2 expect(Export.new(account.reload).total_blocks).to eq 2
end end
it 'returns the total number of the muted accounts' do it 'returns the total number of the muted accounts' do
target_accounts.each(&account.method(:mute!)) target_accounts.each(&account.method(:mute!))
expect(Export.new(account).total_mutes).to eq 2 expect(Export.new(account.reload).total_mutes).to eq 2
end end
end end
end end

View File

@ -37,4 +37,20 @@ RSpec.describe Follow, type: :model do
expect(a[1]).to eq follow0 expect(a[1]).to eq follow0
end end
end end
describe 'revoke_request!' do
let(:follow) { Fabricate(:follow, account: account, target_account: target_account) }
let(:account) { Fabricate(:account) }
let(:target_account) { Fabricate(:account) }
it 'revokes the follow relation' do
follow.revoke_request!
expect(account.following?(target_account)).to be false
end
it 'creates a follow request' do
follow.revoke_request!
expect(account.requested?(target_account)).to be true
end
end
end end

View File

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe StatusStat, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

View File

@ -29,7 +29,7 @@ describe SearchService, type: :service do
allow(ResolveURLService).to receive(:new).and_return(service) allow(ResolveURLService).to receive(:new).and_return(service)
results = subject.call(@query, 10) results = subject.call(@query, 10)
expect(service).to have_received(:call).with(@query) expect(service).to have_received(:call).with(@query, on_behalf_of: nil)
expect(results).to eq empty_results expect(results).to eq empty_results
end end
end end
@ -41,7 +41,7 @@ describe SearchService, type: :service do
allow(ResolveURLService).to receive(:new).and_return(service) allow(ResolveURLService).to receive(:new).and_return(service)
results = subject.call(@query, 10) results = subject.call(@query, 10)
expect(service).to have_received(:call).with(@query) expect(service).to have_received(:call).with(@query, on_behalf_of: nil)
expect(results).to eq empty_results.merge(accounts: [account]) expect(results).to eq empty_results.merge(accounts: [account])
end end
end end
@ -53,7 +53,7 @@ describe SearchService, type: :service do
allow(ResolveURLService).to receive(:new).and_return(service) allow(ResolveURLService).to receive(:new).and_return(service)
results = subject.call(@query, 10) results = subject.call(@query, 10)
expect(service).to have_received(:call).with(@query) expect(service).to have_received(:call).with(@query, on_behalf_of: nil)
expect(results).to eq empty_results.merge(statuses: [status]) expect(results).to eq empty_results.merge(statuses: [status])
end end
end end