Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `spec/models/status_spec.rb`: Upstream added tests too close to glitch-soc-specific tests. Kept both tests.shrike
commit
61ef81c548
|
@ -202,10 +202,6 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
|
||||||
# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
|
# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
|
||||||
# PAM_CONTROLLED_SERVICE=rpam
|
# PAM_CONTROLLED_SERVICE=rpam
|
||||||
|
|
||||||
# Global OAuth settings (optional) :
|
|
||||||
# If you have only one strategy, you may want to enable this
|
|
||||||
# OAUTH_REDIRECT_AT_SIGN_IN=true
|
|
||||||
|
|
||||||
# Optional CAS authentication (cf. omniauth-cas) :
|
# Optional CAS authentication (cf. omniauth-cas) :
|
||||||
# CAS_ENABLED=true
|
# CAS_ENABLED=true
|
||||||
# CAS_URL=https://sso.myserver.com/
|
# CAS_URL=https://sso.myserver.com/
|
||||||
|
|
|
@ -83,10 +83,14 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_enabled_registrations
|
def check_enabled_registrations
|
||||||
forbidden if single_user_mode? || !allowed_registrations?
|
forbidden if single_user_mode? || omniauth_only? || !allowed_registrations?
|
||||||
end
|
end
|
||||||
|
|
||||||
def allowed_registrations?
|
def allowed_registrations?
|
||||||
Setting.registrations_mode != 'none'
|
Setting.registrations_mode != 'none'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def omniauth_only?
|
||||||
|
ENV['OMNIAUTH_ONLY'] == 'true'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -82,13 +82,17 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_enabled_registrations
|
def check_enabled_registrations
|
||||||
redirect_to root_path if single_user_mode? || !allowed_registrations?
|
redirect_to root_path if single_user_mode? || omniauth_only? || !allowed_registrations?
|
||||||
end
|
end
|
||||||
|
|
||||||
def allowed_registrations?
|
def allowed_registrations?
|
||||||
Setting.registrations_mode != 'none' || @invite&.valid_for_use?
|
Setting.registrations_mode != 'none' || @invite&.valid_for_use?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def omniauth_only?
|
||||||
|
ENV['OMNIAUTH_ONLY'] == 'true'
|
||||||
|
end
|
||||||
|
|
||||||
def invite_code
|
def invite_code
|
||||||
if params[:user]
|
if params[:user]
|
||||||
params[:user][:invite_code]
|
params[:user][:invite_code]
|
||||||
|
|
|
@ -15,14 +15,6 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
before_action :set_instance_presenter, only: [:new]
|
before_action :set_instance_presenter, only: [:new]
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
def new
|
|
||||||
Devise.omniauth_configs.each do |provider, config|
|
|
||||||
return redirect_to(omniauth_authorize_path(resource_name, provider)) if config.strategy.redirect_at_sign_in
|
|
||||||
end
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
super do |resource|
|
super do |resource|
|
||||||
# We only need to call this if this hasn't already been
|
# We only need to call this if this hasn't already been
|
||||||
|
@ -89,14 +81,6 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_sign_out_path_for(_resource_or_scope)
|
|
||||||
Devise.omniauth_configs.each_value do |config|
|
|
||||||
return root_path if config.strategy.redirect_at_sign_in
|
|
||||||
end
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def require_no_authentication
|
def require_no_authentication
|
||||||
super
|
super
|
||||||
|
|
||||||
|
|
|
@ -50,13 +50,37 @@ module ApplicationHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def available_sign_up_path
|
def available_sign_up_path
|
||||||
if closed_registrations?
|
if closed_registrations? || omniauth_only?
|
||||||
'https://joinmastodon.org/#getting-started'
|
'https://joinmastodon.org/#getting-started'
|
||||||
else
|
else
|
||||||
new_user_registration_path
|
new_user_registration_path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def omniauth_only?
|
||||||
|
ENV['OMNIAUTH_ONLY'] == 'true'
|
||||||
|
end
|
||||||
|
|
||||||
|
def link_to_login(name = nil, html_options = nil, &block)
|
||||||
|
target = new_user_session_path
|
||||||
|
|
||||||
|
if omniauth_only? && Devise.mappings[:user].omniauthable? && User.omniauth_providers.size == 1
|
||||||
|
target = omniauth_authorize_path(:user, User.omniauth_providers[0])
|
||||||
|
html_options ||= {}
|
||||||
|
html_options[:method] = :post
|
||||||
|
end
|
||||||
|
|
||||||
|
if block_given?
|
||||||
|
link_to(target, html_options, &block)
|
||||||
|
else
|
||||||
|
link_to(name, target, html_options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def provider_sign_in_link(provider)
|
||||||
|
link_to I18n.t("auth.providers.#{provider}", default: provider.to_s.chomp('_oauth2').capitalize), omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post
|
||||||
|
end
|
||||||
|
|
||||||
def open_deletion?
|
def open_deletion?
|
||||||
Setting.open_deletion
|
Setting.open_deletion
|
||||||
end
|
end
|
||||||
|
|
|
@ -88,7 +88,7 @@ export default class Retention extends React.PureComponent {
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{data[0].data.slice(1).map((retention, i) => {
|
{data[0].data.slice(1).map((retention, i) => {
|
||||||
const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].percent - sum)/(k + 1) : sum, 0);
|
const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].rate - sum)/(k + 1) : sum, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td key={retention.date}>
|
<td key={retention.date}>
|
||||||
|
@ -118,8 +118,8 @@ export default class Retention extends React.PureComponent {
|
||||||
|
|
||||||
{cohort.data.slice(1).map(retention => (
|
{cohort.data.slice(1).map(retention => (
|
||||||
<td key={retention.date}>
|
<td key={retention.date}>
|
||||||
<div className={classNames('retention__table__box', `retention__table__box--${roundTo10(retention.percent * 100)}`)}>
|
<div className={classNames('retention__table__box', `retention__table__box--${roundTo10(retention.rate * 100)}`)}>
|
||||||
<FormattedNumber value={retention.percent} style='percent' />
|
<FormattedNumber value={retention.rate} style='percent' />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -61,6 +61,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
onPickEmoji: PropTypes.func.isRequired,
|
onPickEmoji: PropTypes.func.isRequired,
|
||||||
showSearch: PropTypes.bool,
|
showSearch: PropTypes.bool,
|
||||||
anyMedia: PropTypes.bool,
|
anyMedia: PropTypes.bool,
|
||||||
|
isInReply: PropTypes.bool,
|
||||||
singleColumn: PropTypes.bool,
|
singleColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -150,7 +151,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
if (this.props.focusDate !== prevProps.focusDate) {
|
if (this.props.focusDate !== prevProps.focusDate) {
|
||||||
let selectionEnd, selectionStart;
|
let selectionEnd, selectionStart;
|
||||||
|
|
||||||
if (this.props.preselectDate !== prevProps.preselectDate) {
|
if (this.props.preselectDate !== prevProps.preselectDate && this.props.isInReply) {
|
||||||
selectionEnd = this.props.text.length;
|
selectionEnd = this.props.text.length;
|
||||||
selectionStart = this.props.text.search(/\s/) + 1;
|
selectionStart = this.props.text.search(/\s/) + 1;
|
||||||
} else if (typeof this.props.caretPosition === 'number') {
|
} else if (typeof this.props.caretPosition === 'number') {
|
||||||
|
|
|
@ -25,6 +25,7 @@ const mapStateToProps = state => ({
|
||||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||||
|
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
|
|
@ -6,7 +6,7 @@ class Admin::Metrics::Retention
|
||||||
end
|
end
|
||||||
|
|
||||||
class CohortData < ActiveModelSerializers::Model
|
class CohortData < ActiveModelSerializers::Model
|
||||||
attributes :date, :percent, :value
|
attributes :date, :rate, :value
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(start_at, end_at, frequency)
|
def initialize(start_at, end_at, frequency)
|
||||||
|
@ -59,7 +59,7 @@ class Admin::Metrics::Retention
|
||||||
|
|
||||||
current_cohort.data << CohortData.new(
|
current_cohort.data << CohortData.new(
|
||||||
date: row['retention_period'],
|
date: row['retention_period'],
|
||||||
percent: rate.to_f,
|
rate: rate.to_f,
|
||||||
value: value.to_s
|
value: value.to_s
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -429,6 +429,9 @@ class Account < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
DISALLOWED_TSQUERY_CHARACTERS = /['?\\:‘’]/.freeze
|
||||||
|
TEXTSEARCH = "(setweight(to_tsvector('simple', accounts.display_name), 'A') || setweight(to_tsvector('simple', accounts.username), 'B') || setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C'))"
|
||||||
|
|
||||||
def readonly_attributes
|
def readonly_attributes
|
||||||
super - %w(statuses_count following_count followers_count)
|
super - %w(statuses_count following_count followers_count)
|
||||||
end
|
end
|
||||||
|
@ -439,70 +442,29 @@ class Account < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def search_for(terms, limit = 10, offset = 0)
|
def search_for(terms, limit = 10, offset = 0)
|
||||||
textsearch, query = generate_query_for_search(terms)
|
tsquery = generate_query_for_search(terms)
|
||||||
|
|
||||||
sql = <<-SQL.squish
|
sql = <<-SQL.squish
|
||||||
SELECT
|
SELECT
|
||||||
accounts.*,
|
accounts.*,
|
||||||
ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
|
ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
|
||||||
FROM accounts
|
FROM accounts
|
||||||
WHERE #{query} @@ #{textsearch}
|
WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
|
||||||
AND accounts.suspended_at IS NULL
|
AND accounts.suspended_at IS NULL
|
||||||
AND accounts.moved_to_account_id IS NULL
|
AND accounts.moved_to_account_id IS NULL
|
||||||
ORDER BY rank DESC
|
ORDER BY rank DESC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT :limit OFFSET :offset
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
records = find_by_sql([sql, limit, offset])
|
records = find_by_sql([sql, limit: limit, offset: offset, tsquery: tsquery])
|
||||||
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
||||||
records
|
records
|
||||||
end
|
end
|
||||||
|
|
||||||
def advanced_search_for(terms, account, limit = 10, following = false, offset = 0)
|
def advanced_search_for(terms, account, limit = 10, following = false, offset = 0)
|
||||||
textsearch, query = generate_query_for_search(terms)
|
tsquery = generate_query_for_search(terms)
|
||||||
|
sql = advanced_search_for_sql_template(following)
|
||||||
if following
|
records = find_by_sql([sql, id: account.id, limit: limit, offset: offset, tsquery: tsquery])
|
||||||
sql = <<-SQL.squish
|
|
||||||
WITH first_degree AS (
|
|
||||||
SELECT target_account_id
|
|
||||||
FROM follows
|
|
||||||
WHERE account_id = ?
|
|
||||||
UNION ALL
|
|
||||||
SELECT ?
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
accounts.*,
|
|
||||||
(count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
|
|
||||||
FROM accounts
|
|
||||||
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?)
|
|
||||||
WHERE accounts.id IN (SELECT * FROM first_degree)
|
|
||||||
AND #{query} @@ #{textsearch}
|
|
||||||
AND accounts.suspended_at IS NULL
|
|
||||||
AND accounts.moved_to_account_id IS NULL
|
|
||||||
GROUP BY accounts.id
|
|
||||||
ORDER BY rank DESC
|
|
||||||
LIMIT ? OFFSET ?
|
|
||||||
SQL
|
|
||||||
|
|
||||||
records = find_by_sql([sql, account.id, account.id, account.id, limit, offset])
|
|
||||||
else
|
|
||||||
sql = <<-SQL.squish
|
|
||||||
SELECT
|
|
||||||
accounts.*,
|
|
||||||
(count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
|
|
||||||
FROM accounts
|
|
||||||
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
|
|
||||||
WHERE #{query} @@ #{textsearch}
|
|
||||||
AND accounts.suspended_at IS NULL
|
|
||||||
AND accounts.moved_to_account_id IS NULL
|
|
||||||
GROUP BY accounts.id
|
|
||||||
ORDER BY rank DESC
|
|
||||||
LIMIT ? OFFSET ?
|
|
||||||
SQL
|
|
||||||
|
|
||||||
records = find_by_sql([sql, account.id, account.id, limit, offset])
|
|
||||||
end
|
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
||||||
records
|
records
|
||||||
end
|
end
|
||||||
|
@ -524,12 +486,55 @@ class Account < ApplicationRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def generate_query_for_search(terms)
|
def generate_query_for_search(unsanitized_terms)
|
||||||
terms = Arel.sql(connection.quote(terms.gsub(/['?\\:]/, ' ')))
|
terms = unsanitized_terms.gsub(DISALLOWED_TSQUERY_CHARACTERS, ' ')
|
||||||
textsearch = "(setweight(to_tsvector('simple', accounts.display_name), 'A') || setweight(to_tsvector('simple', accounts.username), 'B') || setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C'))"
|
|
||||||
query = "to_tsquery('simple', ''' ' || #{terms} || ' ''' || ':*')"
|
|
||||||
|
|
||||||
[textsearch, query]
|
# The final ":*" is for prefix search.
|
||||||
|
# The trailing space does not seem to fit any purpose, but `to_tsquery`
|
||||||
|
# behaves differently with and without a leading space if the terms start
|
||||||
|
# with `./`, `../`, or `.. `. I don't understand why, so, in doubt, keep
|
||||||
|
# the same query.
|
||||||
|
"' #{terms} ':*"
|
||||||
|
end
|
||||||
|
|
||||||
|
def advanced_search_for_sql_template(following)
|
||||||
|
if following
|
||||||
|
<<-SQL.squish
|
||||||
|
WITH first_degree AS (
|
||||||
|
SELECT target_account_id
|
||||||
|
FROM follows
|
||||||
|
WHERE account_id = :id
|
||||||
|
UNION ALL
|
||||||
|
SELECT :id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
accounts.*,
|
||||||
|
(count(f.id) + 1) * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
|
||||||
|
FROM accounts
|
||||||
|
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id)
|
||||||
|
WHERE accounts.id IN (SELECT * FROM first_degree)
|
||||||
|
AND to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
|
||||||
|
AND accounts.suspended_at IS NULL
|
||||||
|
AND accounts.moved_to_account_id IS NULL
|
||||||
|
GROUP BY accounts.id
|
||||||
|
ORDER BY rank DESC
|
||||||
|
LIMIT :limit OFFSET :offset
|
||||||
|
SQL
|
||||||
|
else
|
||||||
|
<<-SQL.squish
|
||||||
|
SELECT
|
||||||
|
accounts.*,
|
||||||
|
(count(f.id) + 1) * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
|
||||||
|
FROM accounts
|
||||||
|
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id) OR (accounts.id = f.target_account_id AND f.account_id = :id)
|
||||||
|
WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
|
||||||
|
AND accounts.suspended_at IS NULL
|
||||||
|
AND accounts.moved_to_account_id IS NULL
|
||||||
|
GROUP BY accounts.id
|
||||||
|
ORDER BY rank DESC
|
||||||
|
LIMIT :limit OFFSET :offset
|
||||||
|
SQL
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -103,15 +103,12 @@ class Status < ApplicationRecord
|
||||||
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
|
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
|
||||||
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
|
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
|
||||||
scope :tagged_with_all, ->(tag_ids) {
|
scope :tagged_with_all, ->(tag_ids) {
|
||||||
Array(tag_ids).reduce(self) do |result, id|
|
Array(tag_ids).map(&:to_i).reduce(self) do |result, id|
|
||||||
result.joins("INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
|
result.joins("INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
scope :tagged_with_none, ->(tag_ids) {
|
scope :tagged_with_none, ->(tag_ids) {
|
||||||
Array(tag_ids).reduce(self) do |result, id|
|
where('NOT EXISTS (SELECT * FROM statuses_tags forbidden WHERE forbidden.status_id = statuses.id AND forbidden.tag_id IN (?))', tag_ids)
|
||||||
result.joins("LEFT OUTER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
|
|
||||||
.where("t#{id}.tag_id IS NULL")
|
|
||||||
end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scope :not_local_only, -> { where(local_only: [false, nil]) }
|
scope :not_local_only, -> { where(local_only: [false, nil]) }
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
# encrypted_password :string default(""), not null
|
# encrypted_password :string default(""), not null
|
||||||
# reset_password_token :string
|
# reset_password_token :string
|
||||||
# reset_password_sent_at :datetime
|
# reset_password_sent_at :datetime
|
||||||
# remember_created_at :datetime
|
|
||||||
# sign_in_count :integer default(0), not null
|
# sign_in_count :integer default(0), not null
|
||||||
# current_sign_in_at :datetime
|
# current_sign_in_at :datetime
|
||||||
# last_sign_in_at :datetime
|
# last_sign_in_at :datetime
|
||||||
|
@ -32,7 +31,6 @@
|
||||||
# disabled :boolean default(FALSE), not null
|
# disabled :boolean default(FALSE), not null
|
||||||
# moderator :boolean default(FALSE), not null
|
# moderator :boolean default(FALSE), not null
|
||||||
# invite_id :bigint(8)
|
# invite_id :bigint(8)
|
||||||
# remember_token :string
|
|
||||||
# chosen_languages :string is an Array
|
# chosen_languages :string is an Array
|
||||||
# created_by_application_id :bigint(8)
|
# created_by_application_id :bigint(8)
|
||||||
# approved :boolean default(TRUE), not null
|
# approved :boolean default(TRUE), not null
|
||||||
|
@ -44,6 +42,11 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
|
self.ignored_columns = %w(
|
||||||
|
remember_created_at
|
||||||
|
remember_token
|
||||||
|
)
|
||||||
|
|
||||||
include Settings::Extend
|
include Settings::Extend
|
||||||
include UserRoles
|
include UserRoles
|
||||||
|
|
||||||
|
@ -329,10 +332,9 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_password!
|
def reset_password!
|
||||||
# First, change password to something random, invalidate the remember-me token,
|
# First, change password to something random and deactivate all sessions
|
||||||
# and deactivate all sessions
|
|
||||||
transaction do
|
transaction do
|
||||||
update(remember_token: nil, remember_created_at: nil, password: SecureRandom.hex)
|
update(password: SecureRandom.hex)
|
||||||
session_activations.destroy_all
|
session_activations.destroy_all
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ class REST::Admin::CohortSerializer < ActiveModel::Serializer
|
||||||
attributes :period, :frequency
|
attributes :period, :frequency
|
||||||
|
|
||||||
class CohortDataSerializer < ActiveModel::Serializer
|
class CohortDataSerializer < ActiveModel::Serializer
|
||||||
attributes :date, :percent, :value
|
attributes :date, :rate, :value
|
||||||
|
|
||||||
def date
|
def date
|
||||||
object.date.iso8601
|
object.date.iso8601
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
= simple_form_for(new_user, url: user_session_path, namespace: 'login') do |f|
|
- unless omniauth_only?
|
||||||
.fields-group
|
= simple_form_for(new_user, url: user_session_path, namespace: 'login') do |f|
|
||||||
- if use_seamless_external_login?
|
.fields-group
|
||||||
= f.input :email, placeholder: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }, hint: false
|
- if use_seamless_external_login?
|
||||||
- else
|
= f.input :email, placeholder: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }, hint: false
|
||||||
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false
|
- else
|
||||||
|
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false
|
||||||
|
|
||||||
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }, hint: false
|
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }, hint: false
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('auth.login'), type: :submit, class: 'button button-primary'
|
= f.button :button, t('auth.login'), type: :submit, class: 'button button-primary'
|
||||||
|
|
||||||
%p.hint.subtle-hint= link_to t('auth.trouble_logging_in'), new_user_password_path
|
%p.hint.subtle-hint= link_to t('auth.trouble_logging_in'), new_user_password_path
|
||||||
|
|
||||||
|
- if Devise.mappings[:user].omniauthable? and User.omniauth_providers.any?
|
||||||
|
.simple_form.alternative-login
|
||||||
|
%h4= omniauth_only? ? t('auth.log_in_with') : t('auth.or_log_in_with')
|
||||||
|
|
||||||
|
.actions
|
||||||
|
- User.omniauth_providers.each do |provider|
|
||||||
|
= provider_sign_in_link(provider)
|
||||||
|
|
|
@ -27,6 +27,9 @@
|
||||||
·
|
·
|
||||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do
|
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||||
|
- if status.edited?
|
||||||
|
·
|
||||||
|
= t('statuses.edited_at', date: l(status.edited_at))
|
||||||
- if status.discarded?
|
- if status.discarded?
|
||||||
·
|
·
|
||||||
%span.negative-hint= t('admin.statuses.deleted')
|
%span.negative-hint= t('admin.statuses.deleted')
|
||||||
|
|
|
@ -4,24 +4,25 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= render partial: 'shared/og'
|
= render partial: 'shared/og'
|
||||||
|
|
||||||
= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
|
- unless omniauth_only?
|
||||||
.fields-group
|
= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
|
||||||
- if use_seamless_external_login?
|
.fields-group
|
||||||
= f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }, hint: false
|
- if use_seamless_external_login?
|
||||||
- else
|
= f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }, hint: false
|
||||||
= f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false
|
- else
|
||||||
.fields-group
|
= f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false
|
||||||
= f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false
|
.fields-group
|
||||||
|
= f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('auth.login'), type: :submit
|
= f.button :button, t('auth.login'), type: :submit
|
||||||
|
|
||||||
- if devise_mapping.omniauthable? and resource_class.omniauth_providers.any?
|
- if devise_mapping.omniauthable? and resource_class.omniauth_providers.any?
|
||||||
.simple_form.alternative-login
|
.simple_form.alternative-login
|
||||||
%h4= t('auth.or_log_in_with')
|
%h4= omniauth_only? ? t('auth.log_in_with') : t('auth.or_log_in_with')
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
- resource_class.omniauth_providers.each do |provider|
|
- resource_class.omniauth_providers.each do |provider|
|
||||||
= link_to t("auth.providers.#{provider}", default: provider.to_s.chomp("_oauth2").capitalize), omniauth_authorize_path(resource_name, provider), class: "button button-#{provider}", method: :post
|
= provider_sign_in_link(provider)
|
||||||
|
|
||||||
.form-footer= render 'auth/shared/links'
|
.form-footer= render 'auth/shared/links'
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
%li= link_to t('settings.account_settings'), edit_user_registration_path
|
%li= link_to t('settings.account_settings'), edit_user_registration_path
|
||||||
- else
|
- else
|
||||||
- if controller_name != 'sessions'
|
- if controller_name != 'sessions'
|
||||||
%li= link_to t('auth.login'), new_user_session_path
|
%li= link_to_login t('auth.login')
|
||||||
|
|
||||||
- if controller_name != 'registrations'
|
- if controller_name != 'registrations'
|
||||||
%li= link_to t('auth.register'), available_sign_up_path
|
%li= link_to t('auth.register'), available_sign_up_path
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
- if user_signed_in?
|
- if user_signed_in?
|
||||||
= link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn'
|
= link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn'
|
||||||
- else
|
- else
|
||||||
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn nav-link nav-button'
|
= link_to_login t('auth.login'), class: 'webapp-btn nav-link nav-button'
|
||||||
= link_to t('auth.register'), available_sign_up_path, class: 'webapp-btn nav-link nav-button'
|
= link_to t('auth.register'), available_sign_up_path, class: 'webapp-btn nav-link nav-button'
|
||||||
|
|
||||||
.container= yield
|
.container= yield
|
||||||
|
|
|
@ -37,10 +37,15 @@
|
||||||
|
|
||||||
.detailed-status__meta
|
.detailed-status__meta
|
||||||
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
||||||
|
- if status.edited?
|
||||||
|
%data.dt-updated{ value: status.edited_at.to_time.iso8601 }
|
||||||
|
|
||||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do
|
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||||
·
|
·
|
||||||
|
- if status.edited?
|
||||||
|
= t('statuses.edited_at', date: l(status.edited_at))
|
||||||
|
·
|
||||||
%span.detailed-status__visibility-icon
|
%span.detailed-status__visibility-icon
|
||||||
= visibility_icon status
|
= visibility_icon status
|
||||||
·
|
·
|
||||||
|
|
|
@ -7,6 +7,9 @@
|
||||||
%span.status__visibility-icon><
|
%span.status__visibility-icon><
|
||||||
= visibility_icon status
|
= visibility_icon status
|
||||||
%time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
%time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||||
|
- if status.edited?
|
||||||
|
%abbr{ title: t('statuses.edited_at', date: l(status.edited_at.to_date)) }
|
||||||
|
*
|
||||||
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
||||||
|
|
||||||
.p-author.h-card
|
.p-author.h-card
|
||||||
|
|
|
@ -56,6 +56,6 @@
|
||||||
|
|
||||||
- if include_threads && !embedded_view? && !user_signed_in?
|
- if include_threads && !embedded_view? && !user_signed_in?
|
||||||
.entry{ class: entry_classes }
|
.entry{ class: entry_classes }
|
||||||
= link_to new_user_session_path, class: 'load-more load-gap' do
|
= link_to_login class: 'load-more load-gap' do
|
||||||
= fa_icon 'comments'
|
= fa_icon 'comments'
|
||||||
= t('statuses.sign_in_to_participate')
|
= t('statuses.sign_in_to_participate')
|
||||||
|
|
|
@ -60,46 +60,6 @@
|
||||||
"confidence": "High",
|
"confidence": "High",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"warning_type": "SQL Injection",
|
|
||||||
"warning_code": 0,
|
|
||||||
"fingerprint": "6e4051854bb62e2ddbc671f82d6c2328892e1134b8b28105ecba9b0122540714",
|
|
||||||
"check_name": "SQL",
|
|
||||||
"message": "Possible SQL injection",
|
|
||||||
"file": "app/models/account.rb",
|
|
||||||
"line": 484,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
||||||
"code": "find_by_sql([\" WITH first_degree AS (\\n SELECT target_account_id\\n FROM follows\\n WHERE account_id = ?\\n UNION ALL\\n SELECT ?\\n )\\n SELECT\\n accounts.*,\\n (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?)\\n WHERE accounts.id IN (SELECT * FROM first_degree)\\n AND #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n GROUP BY accounts.id\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, account.id, account.id, account.id, limit, offset])",
|
|
||||||
"render_path": null,
|
|
||||||
"location": {
|
|
||||||
"type": "method",
|
|
||||||
"class": "Account",
|
|
||||||
"method": "advanced_search_for"
|
|
||||||
},
|
|
||||||
"user_input": "textsearch",
|
|
||||||
"confidence": "Medium",
|
|
||||||
"note": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"warning_type": "SQL Injection",
|
|
||||||
"warning_code": 0,
|
|
||||||
"fingerprint": "6f075c1484908e3ec9bed21ab7cf3c7866be8da3881485d1c82e13093aefcbd7",
|
|
||||||
"check_name": "SQL",
|
|
||||||
"message": "Possible SQL injection",
|
|
||||||
"file": "app/models/status.rb",
|
|
||||||
"line": 105,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
||||||
"code": "result.joins(\"LEFT OUTER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}\")",
|
|
||||||
"render_path": null,
|
|
||||||
"location": {
|
|
||||||
"type": "method",
|
|
||||||
"class": "Status",
|
|
||||||
"method": null
|
|
||||||
},
|
|
||||||
"user_input": "id",
|
|
||||||
"confidence": "Weak",
|
|
||||||
"note": ""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"warning_type": "SQL Injection",
|
"warning_type": "SQL Injection",
|
||||||
"warning_code": 0,
|
"warning_code": 0,
|
||||||
|
@ -180,26 +140,6 @@
|
||||||
"confidence": "Medium",
|
"confidence": "Medium",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"warning_type": "SQL Injection",
|
|
||||||
"warning_code": 0,
|
|
||||||
"fingerprint": "9251d682c4e2840e1b2fea91e7d758efe2097ecb7f6255c065e3750d25eb178c",
|
|
||||||
"check_name": "SQL",
|
|
||||||
"message": "Possible SQL injection",
|
|
||||||
"file": "app/models/account.rb",
|
|
||||||
"line": 453,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
||||||
"code": "find_by_sql([\" SELECT\\n accounts.*,\\n ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n WHERE #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, limit, offset])",
|
|
||||||
"render_path": null,
|
|
||||||
"location": {
|
|
||||||
"type": "method",
|
|
||||||
"class": "Account",
|
|
||||||
"method": "search_for"
|
|
||||||
},
|
|
||||||
"user_input": "textsearch",
|
|
||||||
"confidence": "Medium",
|
|
||||||
"note": ""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"warning_type": "Redirect",
|
"warning_type": "Redirect",
|
||||||
"warning_code": 18,
|
"warning_code": 18,
|
||||||
|
@ -270,26 +210,6 @@
|
||||||
"confidence": "Weak",
|
"confidence": "Weak",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"warning_type": "SQL Injection",
|
|
||||||
"warning_code": 0,
|
|
||||||
"fingerprint": "e21d8fee7a5805761679877ca35ed1029c64c45ef3b4012a30262623e1ba8bb9",
|
|
||||||
"check_name": "SQL",
|
|
||||||
"message": "Possible SQL injection",
|
|
||||||
"file": "app/models/account.rb",
|
|
||||||
"line": 500,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
||||||
"code": "find_by_sql([\" SELECT\\n accounts.*,\\n (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)\\n WHERE #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n GROUP BY accounts.id\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, account.id, account.id, limit, offset])",
|
|
||||||
"render_path": null,
|
|
||||||
"location": {
|
|
||||||
"type": "method",
|
|
||||||
"class": "Account",
|
|
||||||
"method": "advanced_search_for"
|
|
||||||
},
|
|
||||||
"user_input": "textsearch",
|
|
||||||
"confidence": "Medium",
|
|
||||||
"note": ""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"warning_type": "Mass Assignment",
|
"warning_type": "Mass Assignment",
|
||||||
"warning_code": 105,
|
"warning_code": 105,
|
||||||
|
|
|
@ -5,7 +5,6 @@ end
|
||||||
Devise.setup do |config|
|
Devise.setup do |config|
|
||||||
# Devise omniauth strategies
|
# Devise omniauth strategies
|
||||||
options = {}
|
options = {}
|
||||||
options[:redirect_at_sign_in] = ENV['OAUTH_REDIRECT_AT_SIGN_IN'] == 'true'
|
|
||||||
|
|
||||||
# CAS strategy
|
# CAS strategy
|
||||||
if ENV['CAS_ENABLED'] == 'true'
|
if ENV['CAS_ENABLED'] == 'true'
|
||||||
|
|
|
@ -844,6 +844,7 @@ en:
|
||||||
invalid_reset_password_token: Password reset token is invalid or expired. Please request a new one.
|
invalid_reset_password_token: Password reset token is invalid or expired. Please request a new one.
|
||||||
link_to_otp: Enter a two-factor code from your phone or a recovery code
|
link_to_otp: Enter a two-factor code from your phone or a recovery code
|
||||||
link_to_webauth: Use your security key device
|
link_to_webauth: Use your security key device
|
||||||
|
log_in_with: Log in with
|
||||||
login: Log in
|
login: Log in
|
||||||
logout: Logout
|
logout: Logout
|
||||||
migrate_account: Move to a different account
|
migrate_account: Move to a different account
|
||||||
|
@ -1309,6 +1310,7 @@ en:
|
||||||
disallowed_hashtags:
|
disallowed_hashtags:
|
||||||
one: 'contained a disallowed hashtag: %{tags}'
|
one: 'contained a disallowed hashtag: %{tags}'
|
||||||
other: 'contained the disallowed hashtags: %{tags}'
|
other: 'contained the disallowed hashtags: %{tags}'
|
||||||
|
edited_at: Edited %{date}
|
||||||
errors:
|
errors:
|
||||||
in_reply_not_found: The post you are trying to reply to does not appear to exist.
|
in_reply_not_found: The post you are trying to reply to does not appear to exist.
|
||||||
language_detection: Automatically detect language
|
language_detection: Automatically detect language
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
class RemoveMentionsStatusIdIndex < ActiveRecord::Migration[6.1]
|
||||||
|
def up
|
||||||
|
remove_index :mentions, name: :mentions_status_id_index if index_exists?(:mentions, :status_id, name: :mentions_status_id_index)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# As this index should not exist and is a duplicate of another index, do not re-create it
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoveIndexUsersOnRememberToken < ActiveRecord::Migration[6.1]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
remove_index :users, name: :index_users_on_remember_token
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
add_index :users, :remember_token, algorithm: :concurrently, unique: true, name: :index_users_on_remember_token
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,8 @@
|
||||||
|
class RemoveRememberableFromUsers < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
safety_assured do
|
||||||
|
remove_column :users, :remember_token, :string, null: true, default: nil
|
||||||
|
remove_column :users, :remember_created_at, :datetime, null: true, default: nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -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: 2022_01_16_202951) do
|
ActiveRecord::Schema.define(version: 2022_01_18_183123) 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"
|
||||||
|
@ -939,7 +939,6 @@ ActiveRecord::Schema.define(version: 2022_01_16_202951) do
|
||||||
t.string "encrypted_password", default: "", null: false
|
t.string "encrypted_password", default: "", null: false
|
||||||
t.string "reset_password_token"
|
t.string "reset_password_token"
|
||||||
t.datetime "reset_password_sent_at"
|
t.datetime "reset_password_sent_at"
|
||||||
t.datetime "remember_created_at"
|
|
||||||
t.integer "sign_in_count", default: 0, null: false
|
t.integer "sign_in_count", default: 0, null: false
|
||||||
t.datetime "current_sign_in_at"
|
t.datetime "current_sign_in_at"
|
||||||
t.datetime "last_sign_in_at"
|
t.datetime "last_sign_in_at"
|
||||||
|
@ -961,7 +960,6 @@ ActiveRecord::Schema.define(version: 2022_01_16_202951) do
|
||||||
t.boolean "disabled", default: false, null: false
|
t.boolean "disabled", default: false, null: false
|
||||||
t.boolean "moderator", default: false, null: false
|
t.boolean "moderator", default: false, null: false
|
||||||
t.bigint "invite_id"
|
t.bigint "invite_id"
|
||||||
t.string "remember_token"
|
|
||||||
t.string "chosen_languages", array: true
|
t.string "chosen_languages", array: true
|
||||||
t.bigint "created_by_application_id"
|
t.bigint "created_by_application_id"
|
||||||
t.boolean "approved", default: true, null: false
|
t.boolean "approved", default: true, null: false
|
||||||
|
@ -974,7 +972,6 @@ ActiveRecord::Schema.define(version: 2022_01_16_202951) do
|
||||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||||
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id"
|
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id"
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
t.index ["remember_token"], name: "index_users_on_remember_token", unique: true
|
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
MIN_SUPPORTED_VERSION = 2019_10_01_213028
|
MIN_SUPPORTED_VERSION = 2019_10_01_213028
|
||||||
MAX_SUPPORTED_VERSION = 2021_05_26_193025
|
MAX_SUPPORTED_VERSION = 2022_01_18_183123
|
||||||
|
|
||||||
# Stubs to enjoy ActiveRecord queries while not depending on a particular
|
# Stubs to enjoy ActiveRecord queries while not depending on a particular
|
||||||
# version of the code/database
|
# version of the code/database
|
||||||
|
@ -84,13 +84,14 @@ module Mastodon
|
||||||
|
|
||||||
owned_classes = [
|
owned_classes = [
|
||||||
Status, StatusPin, MediaAttachment, Poll, Report, Tombstone, Favourite,
|
Status, StatusPin, MediaAttachment, Poll, Report, Tombstone, Favourite,
|
||||||
Follow, FollowRequest, Block, Mute, AccountIdentityProof,
|
Follow, FollowRequest, Block, Mute,
|
||||||
AccountModerationNote, AccountPin, AccountStat, ListAccount,
|
AccountModerationNote, AccountPin, AccountStat, ListAccount,
|
||||||
PollVote, Mention
|
PollVote, Mention
|
||||||
]
|
]
|
||||||
owned_classes << AccountDeletionRequest if ActiveRecord::Base.connection.table_exists?(:account_deletion_requests)
|
owned_classes << AccountDeletionRequest if ActiveRecord::Base.connection.table_exists?(:account_deletion_requests)
|
||||||
owned_classes << AccountNote if ActiveRecord::Base.connection.table_exists?(:account_notes)
|
owned_classes << AccountNote if ActiveRecord::Base.connection.table_exists?(:account_notes)
|
||||||
owned_classes << FollowRecommendationSuppression if ActiveRecord::Base.connection.table_exists?(:follow_recommendation_suppressions)
|
owned_classes << FollowRecommendationSuppression if ActiveRecord::Base.connection.table_exists?(:follow_recommendation_suppressions)
|
||||||
|
owned_classes << AccountIdentityProof if ActiveRecord::Base.connection.table_exists?(:account_identity_proofs)
|
||||||
|
|
||||||
owned_classes.each do |klass|
|
owned_classes.each do |klass|
|
||||||
klass.where(account_id: other_account.id).find_each do |record|
|
klass.where(account_id: other_account.id).find_each do |record|
|
||||||
|
@ -139,17 +140,22 @@ module Mastodon
|
||||||
@prompt = TTY::Prompt.new
|
@prompt = TTY::Prompt.new
|
||||||
|
|
||||||
if ActiveRecord::Migrator.current_version < MIN_SUPPORTED_VERSION
|
if ActiveRecord::Migrator.current_version < MIN_SUPPORTED_VERSION
|
||||||
@prompt.warn 'Your version of the database schema is too old and is not supported by this script.'
|
@prompt.error 'Your version of the database schema is too old and is not supported by this script.'
|
||||||
@prompt.warn 'Please update to at least Mastodon 3.0.0 before running this script.'
|
@prompt.error 'Please update to at least Mastodon 3.0.0 before running this script.'
|
||||||
exit(1)
|
exit(1)
|
||||||
elsif ActiveRecord::Migrator.current_version > MAX_SUPPORTED_VERSION
|
elsif ActiveRecord::Migrator.current_version > MAX_SUPPORTED_VERSION
|
||||||
@prompt.warn 'Your version of the database schema is more recent than this script, this may cause unexpected errors.'
|
@prompt.warn 'Your version of the database schema is more recent than this script, this may cause unexpected errors.'
|
||||||
exit(1) unless @prompt.yes?('Continue anyway?')
|
exit(1) unless @prompt.yes?('Continue anyway? (Yes/No)')
|
||||||
|
end
|
||||||
|
|
||||||
|
if Sidekiq::ProcessSet.new.any?
|
||||||
|
@prompt.error 'It seems Sidekiq is running. All Mastodon processes need to be stopped when using this script.'
|
||||||
|
exit(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
@prompt.warn 'This task will take a long time to run and is potentially destructive.'
|
@prompt.warn 'This task will take a long time to run and is potentially destructive.'
|
||||||
@prompt.warn 'Please make sure to stop Mastodon and have a backup.'
|
@prompt.warn 'Please make sure to stop Mastodon and have a backup.'
|
||||||
exit(1) unless @prompt.yes?('Continue?')
|
exit(1) unless @prompt.yes?('Continue? (Yes/No)')
|
||||||
|
|
||||||
deduplicate_users!
|
deduplicate_users!
|
||||||
deduplicate_account_domain_blocks!
|
deduplicate_account_domain_blocks!
|
||||||
|
@ -236,12 +242,14 @@ module Mastodon
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row|
|
if ActiveRecord::Migrator.current_version < 20220118183010
|
||||||
users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse.drop(1)
|
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row|
|
||||||
@prompt.warn "Unsetting remember token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}"
|
users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse.drop(1)
|
||||||
|
@prompt.warn "Unsetting remember token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}"
|
||||||
|
|
||||||
users.each do |user|
|
users.each do |user|
|
||||||
user.update!(remember_token: nil)
|
user.update!(remember_token: nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -257,7 +265,7 @@ module Mastodon
|
||||||
@prompt.say 'Restoring users indexes…'
|
@prompt.say 'Restoring users indexes…'
|
||||||
ActiveRecord::Base.connection.add_index :users, ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true
|
ActiveRecord::Base.connection.add_index :users, ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true
|
||||||
ActiveRecord::Base.connection.add_index :users, ['email'], name: 'index_users_on_email', unique: true
|
ActiveRecord::Base.connection.add_index :users, ['email'], name: 'index_users_on_email', unique: true
|
||||||
ActiveRecord::Base.connection.add_index :users, ['remember_token'], name: 'index_users_on_remember_token', unique: true
|
ActiveRecord::Base.connection.add_index :users, ['remember_token'], name: 'index_users_on_remember_token', unique: true if ActiveRecord::Migrator.current_version < 20220118183010
|
||||||
ActiveRecord::Base.connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true
|
ActiveRecord::Base.connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -274,6 +282,8 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def deduplicate_account_identity_proofs!
|
def deduplicate_account_identity_proofs!
|
||||||
|
return unless ActiveRecord::Base.connection.table_exists?(:account_identity_proofs)
|
||||||
|
|
||||||
remove_index_if_exists!(:account_identity_proofs, 'index_account_proofs_on_account_and_provider_and_username')
|
remove_index_if_exists!(:account_identity_proofs, 'index_account_proofs_on_account_and_provider_and_username')
|
||||||
|
|
||||||
@prompt.say 'Removing duplicate account identity proofs…'
|
@prompt.say 'Removing duplicate account identity proofs…'
|
||||||
|
|
|
@ -441,7 +441,7 @@ namespace :mastodon do
|
||||||
|
|
||||||
namespace :webpush do
|
namespace :webpush do
|
||||||
desc 'Generate VAPID key'
|
desc 'Generate VAPID key'
|
||||||
task generate_vapid_key: :environment do
|
task :generate_vapid_key do
|
||||||
vapid_key = Webpush.generate_key
|
vapid_key = Webpush.generate_key
|
||||||
puts "VAPID_PRIVATE_KEY=#{vapid_key.private_key}"
|
puts "VAPID_PRIVATE_KEY=#{vapid_key.private_key}"
|
||||||
puts "VAPID_PUBLIC_KEY=#{vapid_key.public_key}"
|
puts "VAPID_PUBLIC_KEY=#{vapid_key.public_key}"
|
||||||
|
|
|
@ -354,6 +354,87 @@ RSpec.describe Status, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.tagged_with' do
|
||||||
|
let(:tag1) { Fabricate(:tag) }
|
||||||
|
let(:tag2) { Fabricate(:tag) }
|
||||||
|
let(:tag3) { Fabricate(:tag) }
|
||||||
|
let!(:status1) { Fabricate(:status, tags: [tag1]) }
|
||||||
|
let!(:status2) { Fabricate(:status, tags: [tag2]) }
|
||||||
|
let!(:status3) { Fabricate(:status, tags: [tag3]) }
|
||||||
|
let!(:status4) { Fabricate(:status, tags: []) }
|
||||||
|
let!(:status5) { Fabricate(:status, tags: [tag1, tag2, tag3]) }
|
||||||
|
|
||||||
|
context 'when given one tag' do
|
||||||
|
it 'returns the expected statuses' do
|
||||||
|
expect(Status.tagged_with([tag1.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status5.id]
|
||||||
|
expect(Status.tagged_with([tag2.id]).reorder(:id).pluck(:id).uniq).to eq [status2.id, status5.id]
|
||||||
|
expect(Status.tagged_with([tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status3.id, status5.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when given multiple tags' do
|
||||||
|
it 'returns the expected statuses' do
|
||||||
|
expect(Status.tagged_with([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status2.id, status5.id]
|
||||||
|
expect(Status.tagged_with([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status3.id, status5.id]
|
||||||
|
expect(Status.tagged_with([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status2.id, status3.id, status5.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.tagged_with_all' do
|
||||||
|
let(:tag1) { Fabricate(:tag) }
|
||||||
|
let(:tag2) { Fabricate(:tag) }
|
||||||
|
let(:tag3) { Fabricate(:tag) }
|
||||||
|
let!(:status1) { Fabricate(:status, tags: [tag1]) }
|
||||||
|
let!(:status2) { Fabricate(:status, tags: [tag2]) }
|
||||||
|
let!(:status3) { Fabricate(:status, tags: [tag3]) }
|
||||||
|
let!(:status4) { Fabricate(:status, tags: []) }
|
||||||
|
let!(:status5) { Fabricate(:status, tags: [tag1, tag2]) }
|
||||||
|
|
||||||
|
context 'when given one tag' do
|
||||||
|
it 'returns the expected statuses' do
|
||||||
|
expect(Status.tagged_with_all([tag1.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status5.id]
|
||||||
|
expect(Status.tagged_with_all([tag2.id]).reorder(:id).pluck(:id).uniq).to eq [status2.id, status5.id]
|
||||||
|
expect(Status.tagged_with_all([tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status3.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when given multiple tags' do
|
||||||
|
it 'returns the expected statuses' do
|
||||||
|
expect(Status.tagged_with_all([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to eq [status5.id]
|
||||||
|
expect(Status.tagged_with_all([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq []
|
||||||
|
expect(Status.tagged_with_all([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.tagged_with_none' do
|
||||||
|
let(:tag1) { Fabricate(:tag) }
|
||||||
|
let(:tag2) { Fabricate(:tag) }
|
||||||
|
let(:tag3) { Fabricate(:tag) }
|
||||||
|
let!(:status1) { Fabricate(:status, tags: [tag1]) }
|
||||||
|
let!(:status2) { Fabricate(:status, tags: [tag2]) }
|
||||||
|
let!(:status3) { Fabricate(:status, tags: [tag3]) }
|
||||||
|
let!(:status4) { Fabricate(:status, tags: []) }
|
||||||
|
let!(:status5) { Fabricate(:status, tags: [tag1, tag2, tag3]) }
|
||||||
|
|
||||||
|
context 'when given one tag' do
|
||||||
|
it 'returns the expected statuses' do
|
||||||
|
expect(Status.tagged_with_none([tag1.id]).reorder(:id).pluck(:id).uniq).to eq [status2.id, status3.id, status4.id]
|
||||||
|
expect(Status.tagged_with_none([tag2.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status3.id, status4.id]
|
||||||
|
expect(Status.tagged_with_none([tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status2.id, status4.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when given multiple tags' do
|
||||||
|
it 'returns the expected statuses' do
|
||||||
|
expect(Status.tagged_with_none([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to eq [status3.id, status4.id]
|
||||||
|
expect(Status.tagged_with_none([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status2.id, status4.id]
|
||||||
|
expect(Status.tagged_with_none([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status4.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '.permitted_for' do
|
describe '.permitted_for' do
|
||||||
subject { described_class.permitted_for(target_account, account).pluck(:visibility) }
|
subject { described_class.permitted_for(target_account, account).pluck(:visibility) }
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue