Merge branch 'master' into glitch-soc/merge-upstream
Conflicts: - `app/controllers/application_controller.rb`: Conflict due to theming system. - `app/controllers/oauth/authorizations_controller.rb`: Conflict due to theming system.
This commit is contained in:
@ -2,10 +2,6 @@
|
||||
|
||||
module Admin
|
||||
class CustomEmojisController < BaseController
|
||||
include ObfuscateFilename
|
||||
|
||||
obfuscate_filename [:custom_emoji, :image]
|
||||
|
||||
def index
|
||||
authorize :custom_emoji, :index?
|
||||
|
||||
|
@ -21,11 +21,13 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
||||
def load_accounts
|
||||
return [] if hide_results?
|
||||
|
||||
default_accounts.merge(paginated_follows).to_a
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
|
||||
scope.merge(paginated_follows).to_a
|
||||
end
|
||||
|
||||
def hide_results?
|
||||
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
(@account.user_hides_network? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
|
@ -21,11 +21,13 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
||||
def load_accounts
|
||||
return [] if hide_results?
|
||||
|
||||
default_accounts.merge(paginated_follows).to_a
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
|
||||
scope.merge(paginated_follows).to_a
|
||||
end
|
||||
|
||||
def hide_results?
|
||||
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
(@account.user_hides_network? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
|
@ -4,9 +4,6 @@ class Api::V1::MediaController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:media' }
|
||||
before_action :require_user!
|
||||
|
||||
include ObfuscateFilename
|
||||
obfuscate_filename :file
|
||||
|
||||
respond_to :json
|
||||
|
||||
def create
|
||||
|
@ -17,7 +17,9 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
|
||||
private
|
||||
|
||||
def load_accounts
|
||||
default_accounts.merge(paginated_favourites).to_a
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
|
||||
scope.merge(paginated_favourites).to_a
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
|
@ -17,7 +17,9 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
|
||||
private
|
||||
|
||||
def load_accounts
|
||||
default_accounts.merge(paginated_statuses).to_a
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
|
||||
scope.merge(paginated_statuses).to_a
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
|
@ -25,6 +25,7 @@ class ApplicationController < ActionController::Base
|
||||
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
|
||||
rescue_from ActionController::UnknownFormat, with: :not_acceptable
|
||||
rescue_from ActionController::ParameterMissing, with: :bad_request
|
||||
rescue_from Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
||||
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
||||
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
|
||||
@ -211,7 +212,12 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
def respond_with_error(code)
|
||||
use_pack 'error'
|
||||
render "errors/#{code}", layout: 'error', status: code, formats: [:html]
|
||||
respond_to do |format|
|
||||
format.any do
|
||||
use_pack 'error'
|
||||
render "errors/#{code}", layout: 'error', status: code, formats: [:html]
|
||||
end
|
||||
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -11,6 +11,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
before_action :set_instance_presenter, only: [:new, :create, :update]
|
||||
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
||||
before_action :require_not_suspended!, only: [:update]
|
||||
before_action :set_cache_headers, only: [:edit, :update]
|
||||
|
||||
skip_before_action :require_functional!, only: [:edit, :update]
|
||||
|
||||
@ -114,4 +115,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||
end
|
||||
end
|
||||
|
@ -1,16 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ObfuscateFilename
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def obfuscate_filename(path)
|
||||
before_action do
|
||||
file = params.dig(*path)
|
||||
next if file.nil?
|
||||
|
||||
file.original_filename = SecureRandom.hex(8) + File.extname(file.original_filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,10 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class FiltersController < ApplicationController
|
||||
include Authorization
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_filters, only: :index
|
||||
before_action :set_filter, only: [:edit, :update, :destroy]
|
||||
before_action :set_pack
|
||||
|
@ -19,7 +19,6 @@ class FollowerAccountsController < ApplicationController
|
||||
next if @account.user_hides_network?
|
||||
|
||||
follows
|
||||
@relationships = AccountRelationshipsPresenter.new(follows.map(&:account_id), current_user.account_id) if user_signed_in?
|
||||
end
|
||||
|
||||
format.json do
|
||||
@ -38,7 +37,11 @@ class FollowerAccountsController < ApplicationController
|
||||
private
|
||||
|
||||
def follows
|
||||
@follows ||= Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
|
||||
return @follows if defined?(@follows)
|
||||
|
||||
scope = Follow.where(target_account: @account)
|
||||
scope = scope.where.not(account_id: current_account.excluded_from_timeline_account_ids) if user_signed_in?
|
||||
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
|
||||
end
|
||||
|
||||
def page_requested?
|
||||
|
@ -19,7 +19,6 @@ class FollowingAccountsController < ApplicationController
|
||||
next if @account.user_hides_network?
|
||||
|
||||
follows
|
||||
@relationships = AccountRelationshipsPresenter.new(follows.map(&:target_account_id), current_user.account_id) if user_signed_in?
|
||||
end
|
||||
|
||||
format.json do
|
||||
@ -38,7 +37,11 @@ class FollowingAccountsController < ApplicationController
|
||||
private
|
||||
|
||||
def follows
|
||||
@follows ||= Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
|
||||
return @follows if defined?(@follows)
|
||||
|
||||
scope = Follow.where(account: @account)
|
||||
scope = scope.where.not(target_account_id: current_account.excluded_from_timeline_account_ids) if user_signed_in?
|
||||
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
|
||||
end
|
||||
|
||||
def page_requested?
|
||||
|
@ -6,6 +6,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||
before_action :store_current_location
|
||||
before_action :authenticate_resource_owner!
|
||||
before_action :set_pack
|
||||
before_action :set_cache_headers
|
||||
|
||||
include Localized
|
||||
|
||||
@ -32,4 +33,8 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||
def truthy_param?(key)
|
||||
ActiveModel::Type::Boolean.new.cast(params[key])
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||
end
|
||||
end
|
||||
|
@ -3,6 +3,7 @@
|
||||
class Settings::BaseController < ApplicationController
|
||||
before_action :set_pack
|
||||
before_action :set_body_classes
|
||||
before_action :set_cache_headers
|
||||
|
||||
private
|
||||
|
||||
@ -13,4 +14,8 @@ class Settings::BaseController < ApplicationController
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||
end
|
||||
end
|
||||
|
@ -1,16 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::ProfilesController < Settings::BaseController
|
||||
include ObfuscateFilename
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_account
|
||||
|
||||
obfuscate_filename [:account, :avatar]
|
||||
obfuscate_filename [:account, :header]
|
||||
|
||||
def show
|
||||
@account.build_fields
|
||||
end
|
||||
|
@ -8,12 +8,8 @@ module WellKnown
|
||||
|
||||
def show
|
||||
@webfinger_template = "#{webfinger_url}?resource={uri}"
|
||||
|
||||
respond_to do |format|
|
||||
format.xml { render content_type: 'application/xrd+xml' }
|
||||
end
|
||||
|
||||
expires_in 3.days, public: true
|
||||
render content_type: 'application/xrd+xml', formats: [:xml]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -13,7 +13,7 @@ module AccountsHelper
|
||||
if account.local?
|
||||
"@#{account.acct}@#{Rails.configuration.x.local_domain}"
|
||||
else
|
||||
"@#{account.acct}"
|
||||
"@#{account.pretty_acct}"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -26,8 +26,9 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
|
||||
export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
|
||||
export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
|
||||
|
||||
export const STATUS_REVEAL = 'STATUS_REVEAL';
|
||||
export const STATUS_HIDE = 'STATUS_HIDE';
|
||||
export const STATUS_REVEAL = 'STATUS_REVEAL';
|
||||
export const STATUS_HIDE = 'STATUS_HIDE';
|
||||
export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
|
||||
|
||||
export const REDRAFT = 'REDRAFT';
|
||||
|
||||
@ -320,3 +321,11 @@ export function revealStatus(ids) {
|
||||
ids,
|
||||
};
|
||||
};
|
||||
|
||||
export function toggleStatusCollapse(id, isCollapsed) {
|
||||
return {
|
||||
type: STATUS_COLLAPSE,
|
||||
id,
|
||||
isCollapsed,
|
||||
};
|
||||
}
|
||||
|
@ -208,10 +208,13 @@ export default class ScrollableList extends PureComponent {
|
||||
}
|
||||
|
||||
attachIntersectionObserver () {
|
||||
this.intersectionObserverWrapper.connect({
|
||||
let nodeOptions = {
|
||||
root: this.node,
|
||||
rootMargin: '300% 0px',
|
||||
});
|
||||
};
|
||||
|
||||
this.intersectionObserverWrapper
|
||||
.connect(this.props.bindToDocument ? {} : nodeOptions);
|
||||
}
|
||||
|
||||
detachIntersectionObserver () {
|
||||
|
@ -76,6 +76,7 @@ class Status extends ImmutablePureComponent {
|
||||
onEmbed: PropTypes.func,
|
||||
onHeightChange: PropTypes.func,
|
||||
onToggleHidden: PropTypes.func,
|
||||
onToggleCollapsed: PropTypes.func,
|
||||
muted: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
unread: PropTypes.bool,
|
||||
@ -102,19 +103,6 @@ class Status extends ImmutablePureComponent {
|
||||
statusId: undefined,
|
||||
};
|
||||
|
||||
// Track height changes we know about to compensate scrolling
|
||||
componentDidMount () {
|
||||
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
|
||||
}
|
||||
|
||||
getSnapshotBeforeUpdate () {
|
||||
if (this.props.getScrollPosition) {
|
||||
return this.props.getScrollPosition();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
|
||||
return {
|
||||
@ -126,32 +114,6 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
// Compensate height changes
|
||||
componentDidUpdate (prevProps, prevState, snapshot) {
|
||||
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
|
||||
|
||||
if (doShowCard && !this.didShowCard) {
|
||||
this.didShowCard = true;
|
||||
|
||||
if (snapshot !== null && this.props.updateScrollBottom) {
|
||||
if (this.node && this.node.offsetTop < snapshot.top) {
|
||||
this.props.updateScrollBottom(snapshot.height - snapshot.top);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.node && this.props.getScrollPosition) {
|
||||
const position = this.props.getScrollPosition();
|
||||
if (position !== null && this.node.offsetTop < position.top) {
|
||||
requestAnimationFrame(() => {
|
||||
this.props.updateScrollBottom(position.height - position.top);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleMediaVisibility = () => {
|
||||
this.setState({ showMedia: !this.state.showMedia });
|
||||
}
|
||||
@ -196,7 +158,11 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
handleExpandedToggle = () => {
|
||||
this.props.onToggleHidden(this._properStatus());
|
||||
};
|
||||
}
|
||||
|
||||
handleCollapsedToggle = isCollapsed => {
|
||||
this.props.onToggleCollapsed(this._properStatus(), isCollapsed);
|
||||
}
|
||||
|
||||
renderLoadingMediaGallery () {
|
||||
return <div className='media-gallery' style={{ height: '110px' }} />;
|
||||
@ -466,7 +432,7 @@ class Status extends ImmutablePureComponent {
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable />
|
||||
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} />
|
||||
|
||||
{media}
|
||||
|
||||
|
@ -23,11 +23,11 @@ export default class StatusContent extends React.PureComponent {
|
||||
onExpandedToggle: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
collapsable: PropTypes.bool,
|
||||
onCollapsedToggle: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
hidden: true,
|
||||
collapsed: null, // `collapsed: null` indicates that an element doesn't need collapsing, while `true` or `false` indicates that it does (and is/isn't).
|
||||
};
|
||||
|
||||
_updateStatusLinks () {
|
||||
@ -62,14 +62,16 @@ export default class StatusContent extends React.PureComponent {
|
||||
link.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.collapsable
|
||||
&& this.props.onClick
|
||||
&& this.state.collapsed === null
|
||||
&& node.clientHeight > MAX_HEIGHT
|
||||
&& this.props.status.get('spoiler_text').length === 0
|
||||
) {
|
||||
this.setState({ collapsed: true });
|
||||
if (this.props.status.get('collapsed', null) === null) {
|
||||
let collapsed =
|
||||
this.props.collapsable
|
||||
&& this.props.onClick
|
||||
&& node.clientHeight > MAX_HEIGHT
|
||||
&& this.props.status.get('spoiler_text').length === 0;
|
||||
|
||||
if(this.props.onCollapsedToggle) this.props.onCollapsedToggle(collapsed);
|
||||
|
||||
this.props.status.set('collapsed', collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,6 +180,7 @@ export default class StatusContent extends React.PureComponent {
|
||||
}
|
||||
|
||||
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
||||
const renderReadMore = this.props.onClick && status.get('collapsed');
|
||||
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
const spoilerContent = { __html: status.get('spoilerHtml') };
|
||||
@ -185,7 +188,7 @@ export default class StatusContent extends React.PureComponent {
|
||||
const classNames = classnames('status__content', {
|
||||
'status__content--with-action': this.props.onClick && this.context.router,
|
||||
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
||||
'status__content--collapsed': this.state.collapsed === true,
|
||||
'status__content--collapsed': renderReadMore,
|
||||
});
|
||||
|
||||
if (isRtl(status.get('search_index'))) {
|
||||
@ -237,7 +240,7 @@ export default class StatusContent extends React.PureComponent {
|
||||
</div>,
|
||||
];
|
||||
|
||||
if (this.state.collapsed) {
|
||||
if (renderReadMore) {
|
||||
output.push(readMoreButton);
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
deleteStatus,
|
||||
hideStatus,
|
||||
revealStatus,
|
||||
toggleStatusCollapse,
|
||||
} from '../actions/statuses';
|
||||
import {
|
||||
unmuteAccount,
|
||||
@ -190,6 +191,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
}
|
||||
},
|
||||
|
||||
onToggleCollapsed (status, isCollapsed) {
|
||||
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
|
||||
},
|
||||
|
||||
onBlockDomain (domain) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
STATUS_UNMUTE_SUCCESS,
|
||||
STATUS_REVEAL,
|
||||
STATUS_HIDE,
|
||||
STATUS_COLLAPSE,
|
||||
} from '../actions/statuses';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
|
||||
@ -73,6 +74,8 @@ export default function statuses(state = initialState, action) {
|
||||
}
|
||||
});
|
||||
});
|
||||
case STATUS_COLLAPSE:
|
||||
return state.setIn([action.id, 'collapsed'], action.isCollapsed);
|
||||
case TIMELINE_DELETE:
|
||||
return deleteStatus(state, action.id, action.references);
|
||||
default:
|
||||
|
@ -4,9 +4,13 @@ import { FormattedNumber } from 'react-intl';
|
||||
export const shortNumberFormat = number => {
|
||||
if (number < 1000) {
|
||||
return <FormattedNumber value={number} />;
|
||||
} else if (number < 1000000) {
|
||||
} else if (number < 10000) {
|
||||
return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</Fragment>;
|
||||
} else {
|
||||
} else if (number < 1000000) {
|
||||
return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={0} />K</Fragment>;
|
||||
} else if (number < 10000000) {
|
||||
return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={1} />M</Fragment>;
|
||||
} else {
|
||||
return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={0} />M</Fragment>;
|
||||
}
|
||||
};
|
||||
|
@ -6399,13 +6399,13 @@ noscript {
|
||||
&__links {
|
||||
font-size: 14px;
|
||||
color: $darker-text-color;
|
||||
padding: 10px 0;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
color: $darker-text-color;
|
||||
text-decoration: none;
|
||||
padding: 10px;
|
||||
padding-top: 20px;
|
||||
padding: 5px 10px;
|
||||
font-weight: 500;
|
||||
|
||||
strong {
|
||||
|
@ -78,7 +78,7 @@ class SearchQueryTransformer < Parslet::Transform
|
||||
elsif clause[:shortcode]
|
||||
TermClause.new(prefix, operator, ":#{clause[:term]}:")
|
||||
elsif clause[:phrase]
|
||||
PhraseClause.new(prefix, operator, clause[:phrase].map { |p| p[:term].to_s }.join(' '))
|
||||
PhraseClause.new(prefix, operator, clause[:phrase].is_a?(Array) ? clause[:phrase].map { |p| p[:term].to_s }.join(' ') : clause[:phrase].to_s)
|
||||
else
|
||||
raise "Unexpected clause type: #{clause}"
|
||||
end
|
||||
|
18
app/middleware/handle_bad_encoding_middleware.rb
Normal file
18
app/middleware/handle_bad_encoding_middleware.rb
Normal file
@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
# See: https://jamescrisp.org/2018/05/28/fixing-invalid-query-parameters-invalid-encoding-in-a-rails-app/
|
||||
|
||||
class HandleBadEncodingMiddleware
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
begin
|
||||
Rack::Utils.parse_nested_query(env['QUERY_STRING'].to_s)
|
||||
rescue Rack::Utils::InvalidParameterError
|
||||
env['QUERY_STRING'] = ''
|
||||
end
|
||||
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
@ -50,7 +50,7 @@
|
||||
|
||||
class Account < ApplicationRecord
|
||||
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
|
||||
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
|
||||
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[a-z0-9]+)?)/i
|
||||
|
||||
include AccountAssociations
|
||||
include AccountAvatar
|
||||
@ -168,6 +168,10 @@ class Account < ApplicationRecord
|
||||
local? ? username : "#{username}@#{domain}"
|
||||
end
|
||||
|
||||
def pretty_acct
|
||||
local? ? username : "#{username}@#{Addressable::IDNA.to_unicode(domain)}"
|
||||
end
|
||||
|
||||
def local_username_and_domain
|
||||
"#{username}@#{Rails.configuration.x.local_domain}"
|
||||
end
|
||||
|
@ -9,6 +9,7 @@ module Attachmentable
|
||||
GIF_MATRIX_LIMIT = 921_600 # 1280x720px
|
||||
|
||||
included do
|
||||
before_post_process :obfuscate_file_name
|
||||
before_post_process :set_file_extensions
|
||||
before_post_process :check_image_dimensions
|
||||
before_post_process :set_file_content_type
|
||||
@ -68,4 +69,14 @@ module Attachmentable
|
||||
rescue Terrapin::CommandLineError
|
||||
''
|
||||
end
|
||||
|
||||
def obfuscate_file_name
|
||||
self.class.attachment_definitions.each_key do |attachment_name|
|
||||
attachment = send(attachment_name)
|
||||
|
||||
next if attachment.blank?
|
||||
|
||||
attachment.instance_write :file_name, SecureRandom.hex(8) + File.extname(attachment.instance_read(:file_name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -202,9 +202,12 @@ class MediaAttachment < ApplicationRecord
|
||||
end
|
||||
|
||||
after_commit :reset_parent_cache, on: :update
|
||||
|
||||
before_create :prepare_description, unless: :local?
|
||||
before_create :set_shortcode
|
||||
|
||||
before_post_process :set_type_and_extension
|
||||
|
||||
before_save :set_meta
|
||||
|
||||
class << self
|
||||
|
@ -24,6 +24,10 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
||||
object.id.to_s
|
||||
end
|
||||
|
||||
def acct
|
||||
object.pretty_acct
|
||||
end
|
||||
|
||||
def note
|
||||
Formatter.instance.simplified_format(object)
|
||||
end
|
||||
|
@ -166,7 +166,7 @@ class BackupService < BaseService
|
||||
io.write(buffer)
|
||||
end
|
||||
end
|
||||
rescue Errno::ENOENT
|
||||
rescue Errno::ENOENT, Seahorse::Client::NetworkingError
|
||||
Rails.logger.warn "Could not backup file #{filename}: file not found"
|
||||
end
|
||||
end
|
||||
|
@ -14,7 +14,16 @@ class ProcessMentionsService < BaseService
|
||||
mentions = []
|
||||
|
||||
status.text = status.text.gsub(Account::MENTION_RE) do |match|
|
||||
username, domain = Regexp.last_match(1).split('@')
|
||||
username, domain = Regexp.last_match(1).split('@')
|
||||
|
||||
domain = begin
|
||||
if TagManager.instance.local_domain?(domain)
|
||||
nil
|
||||
else
|
||||
TagManager.instance.normalize_domain(domain)
|
||||
end
|
||||
end
|
||||
|
||||
mentioned_account = Account.find_remote(username, domain)
|
||||
|
||||
if mention_undeliverable?(mentioned_account)
|
||||
|
@ -12,6 +12,8 @@ class ResolveURLService < BaseService
|
||||
process_local_url
|
||||
elsif !fetched_resource.nil?
|
||||
process_url
|
||||
elsif @on_behalf_of.present?
|
||||
process_url_from_db
|
||||
end
|
||||
end
|
||||
|
||||
@ -24,15 +26,19 @@ class ResolveURLService < BaseService
|
||||
status = FetchRemoteStatusService.new.call(resource_url, body)
|
||||
authorize_with @on_behalf_of, status, :show? unless status.nil?
|
||||
status
|
||||
elsif fetched_resource.nil? && @on_behalf_of.present?
|
||||
# It may happen that the resource is a private toot, and thus not fetchable,
|
||||
# but we can return the toot if we already know about it.
|
||||
status = Status.find_by(uri: @url) || Status.find_by(url: @url)
|
||||
authorize_with @on_behalf_of, status, :show? unless status.nil?
|
||||
status
|
||||
end
|
||||
end
|
||||
|
||||
def process_url_from_db
|
||||
# It may happen that the resource is a private toot, and thus not fetchable,
|
||||
# but we can return the toot if we already know about it.
|
||||
status = Status.find_by(uri: @url) || Status.find_by(url: @url)
|
||||
authorize_with @on_behalf_of, status, :show? unless status.nil?
|
||||
status
|
||||
rescue Mastodon::NotPermittedError
|
||||
nil
|
||||
end
|
||||
|
||||
def fetched_resource
|
||||
@fetched_resource ||= FetchResourceService.new.call(@url)
|
||||
end
|
||||
|
@ -1,6 +1,7 @@
|
||||
.batch-table__row
|
||||
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
|
||||
= f.check_box :tag_ids, { multiple: true, include_hidden: false }, tag.id
|
||||
- if batch_available
|
||||
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
|
||||
= f.check_box :tag_ids, { multiple: true, include_hidden: false }, tag.id
|
||||
|
||||
.directory__tag
|
||||
= link_to admin_tag_path(tag.id) do
|
||||
|
@ -47,25 +47,26 @@
|
||||
|
||||
.batch-table.optional
|
||||
.batch-table__toolbar
|
||||
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||
= check_box_tag :batch_checkbox_all, nil, false
|
||||
.batch-table__toolbar__actions
|
||||
- if params[:pending_review] == '1'
|
||||
- if params[:pending_review] == '1' || params[:unreviewed] == '1'
|
||||
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||
= check_box_tag :batch_checkbox_all, nil, false
|
||||
.batch-table__toolbar__actions
|
||||
= f.button safe_join([fa_icon('check'), t('admin.accounts.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||
|
||||
= f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||
- else
|
||||
- else
|
||||
.batch-table__toolbar__actions
|
||||
%span.neutral-hint= t('generic.no_batch_actions_available')
|
||||
|
||||
.batch-table__body
|
||||
- if @tags.empty?
|
||||
= nothing_here 'nothing-here--under-tabs'
|
||||
- else
|
||||
= render partial: 'tag', collection: @tags, locals: { f: f }
|
||||
= render partial: 'tag', collection: @tags, locals: { f: f, batch_available: params[:pending_review] == '1' || params[:unreviewed] == '1' }
|
||||
|
||||
= paginate @tags
|
||||
|
||||
- if params[:pending_review] == '1'
|
||||
- if params[:pending_review] == '1' || params[:unreviewed] == '1'
|
||||
%hr.spacer/
|
||||
|
||||
%div{ style: 'overflow: hidden' }
|
||||
|
@ -5,6 +5,10 @@
|
||||
.fields-group
|
||||
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, selected: I18n.locale, hint: false
|
||||
|
||||
- unless I18n.locale == :en
|
||||
.flash-message{ style: "text-align: unset; color: unset" }
|
||||
#{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: "_blank", rel: "noopener", style: "text-decoration: underline")}
|
||||
|
||||
%h4= t 'appearance.advanced_web_interface'
|
||||
|
||||
%p.hint= t 'appearance.advanced_web_interface_hint'
|
||||
|
@ -1,3 +1,2 @@
|
||||
- cache @status do
|
||||
.activity-stream.activity-stream--headless
|
||||
= render 'status', status: @status, centered: true, autoplay: @autoplay
|
||||
.activity-stream.activity-stream--headless
|
||||
= render 'status', status: @status, centered: true, autoplay: @autoplay
|
||||
|
@ -7,15 +7,18 @@ class RefollowWorker
|
||||
|
||||
def perform(target_account_id)
|
||||
target_account = Account.find(target_account_id)
|
||||
return unless target_account.protocol == :activitypub
|
||||
return unless target_account.activitypub?
|
||||
|
||||
target_account.passive_relationships.where(account: Account.where(domain: nil)).includes(:account).reorder(nil).find_each do |follow|
|
||||
reblogs = follow.show_reblogs?
|
||||
|
||||
target_account.followers.where(domain: nil).reorder(nil).find_each do |follower|
|
||||
# Locally unfollow remote account
|
||||
follower = follow.account
|
||||
follower.unfollow!(target_account)
|
||||
|
||||
# Schedule re-follow
|
||||
begin
|
||||
FollowService.new.call(follower, target_account)
|
||||
FollowService.new.call(follower, target_account, reblogs: reblogs)
|
||||
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError
|
||||
next
|
||||
end
|
||||
|
Reference in New Issue
Block a user